Skip to content

Commit f0754e9

Browse files
committed
fmpz_mat: add fraction-free LU decomposition
1 parent 7786fc0 commit f0754e9

File tree

2 files changed

+293
-5
lines changed

2 files changed

+293
-5
lines changed

src/flint/test/test_all.py

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import sys
21
import math
32
import operator
43
import pickle
5-
import doctest
64
import platform
5+
import random
76

87
from flint.utils.flint_exceptions import DomainError, IncompatibleContextError
98

@@ -1871,7 +1870,6 @@ def test_fmpz_mod_dlog():
18711870
p = 2**e2 * 3**e3 + 1
18721871
F = fmpz_mod_ctx(p)
18731872

1874-
import random
18751873
for _ in range(10):
18761874
g = F(random.randint(0,p))
18771875
for _ in range(10):
@@ -4084,6 +4082,50 @@ def test_matrices_div():
40844082
raises(lambda: None / M1234, TypeError)
40854083

40864084

4085+
def test_matrices_properties():
4086+
for M, S, is_field in _all_matrices():
4087+
# XXX: Add these properties to all matrix types
4088+
if M is not flint.fmpz_mat:
4089+
continue
4090+
4091+
assert M([[1, 2], [3, 4]]).is_square() is True
4092+
assert M([[1, 2]]).is_square() is False
4093+
assert M(0, 2, []).is_square() is False
4094+
assert M(2, 0, []).is_square() is False
4095+
4096+
assert M([[1, 2], [3, 4]]).is_empty() is False
4097+
assert M(0, 2, []).is_empty() is True
4098+
assert M(2, 0, []).is_empty() is True
4099+
4100+
assert M([[1, 2], [3, 4]]).is_zero() is False
4101+
assert M([[0, 0], [0, 0]]).is_zero() is True
4102+
4103+
assert M([[1, 0], [0, 1]]).is_one() is True
4104+
assert M([[1, 2], [3, 4]]).is_one() is False
4105+
assert M([[1, 0], [0, 1], [0, 0]]).is_one() is True # ??
4106+
assert M(0, 0, []).is_one() is True
4107+
4108+
assert M([[-1, 0], [0, -1]]).is_neg_one() is True
4109+
assert M([[-1, 0], [0, 1]]).is_neg_one() is False
4110+
assert M([[-1, -1], [-1, -1]]).is_neg_one() is False
4111+
assert M([[-1, 0], [0, -1], [0, 0]]).is_neg_one() is False # ??
4112+
assert M(0, 0, []).is_neg_one() is True
4113+
4114+
assert M([[2, 0], [0, 2]]).is_scalar() is True
4115+
assert M([[2, 0], [0, 3]]).is_scalar() is False
4116+
assert M([[1, 0], [0, 1], [0, 0]]).is_scalar() is False
4117+
4118+
assert M([[1, 0], [0, 2]]).is_diagonal() is True
4119+
assert M([[1, 0], [1, 2]]).is_diagonal() is False
4120+
assert M([[1, 0], [0, 1], [0, 0]]).is_diagonal() is True
4121+
4122+
assert M([[1, 1, 1], [0, 2, 2]]).is_upper_triangular() is True
4123+
assert M([[1, 1, 1], [1, 2, 2]]).is_upper_triangular() is False
4124+
4125+
assert M([[1, 0, 0], [1, 2, 0]]).is_lower_triangular() is True
4126+
assert M([[1, 1, 0], [1, 2, 0]]).is_lower_triangular() is False
4127+
4128+
40874129
def test_matrices_inv():
40884130
for M, S, is_field in _all_matrices():
40894131
if is_field:
@@ -4151,6 +4193,63 @@ def test_matrices_rref():
41514193
assert Mr == Mr_rref
41524194

41534195

4196+
def test_matrices_fflu():
4197+
4198+
QQ = flint.fmpq_mat
4199+
shape = lambda A: (A.nrows(), A.ncols())
4200+
4201+
def is_permutation(P):
4202+
if not P.is_square():
4203+
return False
4204+
n = P.nrows()
4205+
for i, row in enumerate(sorted(P.tolist(), reverse=True)):
4206+
if row != [int(i == j) for j in range(n)]:
4207+
return False
4208+
return True
4209+
4210+
def check_fflu(A):
4211+
m, n = shape(A)
4212+
P, L, D, U = A.fflu()
4213+
Dq = QQ(D)
4214+
assert P*A == L*Dq.inv()*U
4215+
assert shape(P) == shape(L) == shape(D) == (m, m)
4216+
assert shape(A) == shape(U) == (m, n)
4217+
assert is_permutation(P)
4218+
assert L.is_lower_triangular()
4219+
assert U.is_upper_triangular()
4220+
assert D.is_diagonal()
4221+
4222+
for M, S, is_field in _all_matrices():
4223+
# XXX: Add this to more matrix types...
4224+
if M is not flint.fmpz_mat:
4225+
continue
4226+
4227+
A = M([[1, 2], [3, 4]])
4228+
P, L, D, U = A.fflu()
4229+
assert P == M([[1, 0], [0, 1]])
4230+
assert L == M([[1, 0], [3, -2]])
4231+
assert D == M([[1, 0], [0, -2]])
4232+
assert U == M([[1, 2], [0, -2]])
4233+
4234+
check_fflu(M(0, 0, []))
4235+
check_fflu(M(2, 0, []))
4236+
check_fflu(M(0, 2, []))
4237+
check_fflu(M([[1]]))
4238+
4239+
check_fflu(M([[1, 2], [3, 4]]))
4240+
check_fflu(M([[1, 2, 3], [4, 5, 6]]))
4241+
check_fflu(M([[1, 2], [3, 4], [5, 6]]))
4242+
check_fflu(M([[1, 2], [2, 4]]))
4243+
check_fflu(M([[0, 0], [0, 0]]))
4244+
check_fflu(M([[1, 1, 1], [1, 1, 1]]))
4245+
4246+
for _ in range(10):
4247+
for m in range(1, 5):
4248+
for n in range(1, 5):
4249+
A = M.randbits(m, n, 10)
4250+
check_fflu(A)
4251+
4252+
41544253
def test_matrices_solve():
41554254
for M, S, is_field in _all_matrices():
41564255
if is_field:
@@ -4619,13 +4718,15 @@ def test_all_tests():
46194718
test_matrices_mul,
46204719
test_matrices_pow,
46214720
test_matrices_div,
4721+
test_matrices_properties,
46224722
test_matrices_inv,
46234723
test_matrices_det,
46244724
test_matrices_charpoly,
46254725
test_matrices_minpoly,
46264726
test_matrices_rank,
46274727
test_matrices_rref,
46284728
test_matrices_solve,
4729+
test_matrices_fflu,
46294730

46304731
test_fq_default,
46314732
test_fq_default_poly,

src/flint/types/fmpz_mat.pyx

Lines changed: 189 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,17 @@ from flint.types.fmpz cimport any_as_fmpz
77
from flint.pyflint cimport global_random_state
88
from flint.types.fmpq cimport any_as_fmpq
99
cimport cython
10-
11-
from flint.flintlib.functions.fmpz cimport fmpz_set, fmpz_init, fmpz_clear
10+
cimport libc.stdlib
11+
12+
from flint.flintlib.functions.fmpz cimport (
13+
fmpz_set,
14+
fmpz_init,
15+
fmpz_clear,
16+
fmpz_set_si,
17+
fmpz_mul,
18+
fmpz_equal,
19+
fmpz_equal_si,
20+
)
1221
from flint.flintlib.functions.fmpz cimport fmpz_is_zero, fmpz_is_pm1
1322
from flint.flintlib.types.fmpz cimport (
1423
fmpz_mat_struct,
@@ -316,6 +325,62 @@ cdef class fmpz_mat(flint_mat):
316325
fmpz_mat_pow(t.val, t.val, ee)
317326
return t
318327

328+
def is_square(self):
329+
"""Return whether *self* is a square *NxN* matrix."""
330+
return bool(fmpz_mat_is_square(self.val))
331+
332+
def is_empty(self):
333+
"""Return whether *self* is an empty *0xN* or *Nx0* matrix."""
334+
return bool(fmpz_mat_is_empty(self.val))
335+
336+
def is_zero(self):
337+
"""Return whether *self* is a zero matrix."""
338+
return bool(fmpz_mat_is_zero(self.val))
339+
340+
def is_one(self):
341+
"""Return whether *self* is the identity matrix."""
342+
return bool(fmpz_mat_is_one(self.val))
343+
344+
def is_neg_one(self):
345+
"""Return whether *self* is the negative identity matrix."""
346+
if not self.is_square():
347+
return False
348+
elif not self.is_scalar():
349+
return False
350+
elif self.is_empty():
351+
return True
352+
else:
353+
return bool(fmpz_equal_si(fmpz_mat_entry(self.val, 0, 0), -1))
354+
355+
def is_upper_triangular(self):
356+
"""Return whether *self* is an upper triangular matrix."""
357+
for i in range(1, fmpz_mat_nrows(self.val)):
358+
for j in range(min(i, fmpz_mat_ncols(self.val))):
359+
if not fmpz_is_zero(fmpz_mat_entry(self.val, i, j)):
360+
return False
361+
return True
362+
363+
def is_lower_triangular(self):
364+
"""Return whether *self* is a lower triangular matrix."""
365+
for i in range(fmpz_mat_nrows(self.val)):
366+
for j in range(i + 1, fmpz_mat_ncols(self.val)):
367+
if not fmpz_is_zero(fmpz_mat_entry(self.val, i, j)):
368+
return False
369+
return True
370+
371+
def is_diagonal(self):
372+
"""Return whether *self* is a diagonal matrix."""
373+
return self.is_upper_triangular() and self.is_lower_triangular()
374+
375+
def is_scalar(self):
376+
"""Return whether *self* is a scalar multiple of the identity matrix."""
377+
if not (self.is_square() and self.is_diagonal()):
378+
return False
379+
for i in range(fmpz_mat_nrows(self.val)):
380+
if not fmpz_equal(fmpz_mat_entry(self.val, i, i), fmpz_mat_entry(self.val, 0, 0)):
381+
return False
382+
return True
383+
319384
@classmethod
320385
def hadamard(cls, ulong n):
321386
"""
@@ -563,6 +628,128 @@ cdef class fmpz_mat(flint_mat):
563628
raise ZeroDivisionError("singular matrix in solve()")
564629
return u
565630

631+
def _fflu(self):
632+
"""
633+
Fraction-free LU decomposition of *self*.
634+
635+
>>> A = fmpz_mat([[1, 2], [3, 4]])
636+
>>> LU, d, perm, rank = A._fflu()
637+
>>> LU
638+
[1, 2]
639+
[3, -2]
640+
>>> d
641+
-2
642+
>>> perm
643+
[0, 1]
644+
>>> rank
645+
2
646+
647+
The matrix *LU* is the LU contains both the lower and upper parts of
648+
the decomposition. The integer *d* is the divisor and is up to a sign
649+
the determinant when *self* is square. The list *perm* is the
650+
permutation of the rows of *self*.
651+
652+
This is the raw output from the underlying FLINT function fmpz_mat_fflu.
653+
The method :meth:`fflu` provides a more understandable representation
654+
of the decomposition.
655+
656+
"""
657+
cdef fmpz d
658+
cdef slong* perm
659+
cdef slong r, c, rank
660+
cdef fmpz_mat LU
661+
r = fmpz_mat_nrows(self.val)
662+
c = fmpz_mat_ncols(self.val)
663+
perm = <slong*>libc.stdlib.malloc(r * sizeof(slong))
664+
if perm is NULL:
665+
raise MemoryError("malloc failed")
666+
try:
667+
for i in range(r):
668+
perm[i] = i
669+
LU = fmpz_mat.__new__(fmpz_mat)
670+
fmpz_mat_init((<fmpz_mat>LU).val, r, c)
671+
d = fmpz.__new__(fmpz)
672+
rank = fmpz_mat_fflu(LU.val, d.val, perm, self.val, 0)
673+
perm_int = []
674+
for i in range(r):
675+
perm_int.append(perm[i])
676+
finally:
677+
libc.stdlib.free(perm)
678+
679+
return LU, d, perm_int, rank
680+
681+
def fflu(self):
682+
"""
683+
Fraction-free LU decomposition of *self*.
684+
685+
Returns a tuple (*P*, *L*, *D*, *U*) representing the the fraction-free
686+
LU decomposition of a matrix *A* as
687+
688+
P*A = L*inv(D)*U
689+
690+
where *P* is a permutation matrix, *L* is lower triangular, *D* is
691+
diagonal and *U* is upper triangular.
692+
693+
>>> A = fmpz_mat([[1, 2], [3, 4]])
694+
>>> P, L, D, U = A.fflu()
695+
>>> P
696+
[1, 0]
697+
[0, 1]
698+
>>> L
699+
[1, 0]
700+
[3, -2]
701+
>>> D
702+
[1, 0]
703+
[0, -2]
704+
>>> U
705+
[1, 2]
706+
[0, -2]
707+
>>> P*A == L*D.inv()*U
708+
True
709+
710+
This method works for matrices of any shape and rank.
711+
712+
"""
713+
cdef slong r, c
714+
cdef slong i, j, k, l
715+
cdef fmpz di
716+
cdef fmpz_mat P, L, U, D
717+
r = fmpz_mat_nrows(self.val)
718+
c = fmpz_mat_ncols(self.val)
719+
720+
U, _d, perm, _rank = self._fflu()
721+
722+
P = fmpz_mat(r, r)
723+
for i, pi in enumerate(perm):
724+
fmpz_set_si(fmpz_mat_entry(P.val, i, pi), 1)
725+
726+
L = fmpz_mat(r, r)
727+
728+
i = j = k = 0
729+
while i < r and j < c:
730+
if not fmpz_is_zero(fmpz_mat_entry(U.val, i, j)):
731+
fmpz_set(fmpz_mat_entry(L.val, i, k), fmpz_mat_entry(U.val, i, j))
732+
for l in range(i + 1, r):
733+
fmpz_set(fmpz_mat_entry(L.val, l, k), fmpz_mat_entry(U.val, l, j))
734+
fmpz_set_si(fmpz_mat_entry(U.val, l, j), 0)
735+
i += 1
736+
k += 1
737+
j += 1
738+
739+
for k in range(k, r):
740+
fmpz_set_si(fmpz_mat_entry(L.val, k, k), 1)
741+
742+
D = fmpz_mat(r, r)
743+
744+
if r >= 1:
745+
fmpz_set(fmpz_mat_entry(D.val, 0, 0), fmpz_mat_entry(L.val, 0, 0))
746+
di = fmpz(1)
747+
for i in range(1, r):
748+
fmpz_mul(di.val, fmpz_mat_entry(L.val, i-1, i-1), fmpz_mat_entry(L.val, i, i))
749+
fmpz_set(fmpz_mat_entry(D.val, i, i), di.val)
750+
751+
return P, L, D, U
752+
566753
def rref(self, inplace=False):
567754
"""
568755
Computes the reduced row echelon form (rref) of *self*,

0 commit comments

Comments
 (0)