Skip to content

Commit 34dbb5d

Browse files
Merge pull request #261 from oscarbenjamin/pr_fflu
fmpz_mat: add fraction-free LU decomposition
2 parents f44a589 + 09850de commit 34dbb5d

File tree

3 files changed

+298
-5
lines changed

3 files changed

+298
-5
lines changed

src/flint/flintlib/types/flint.pxd

+3
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ cdef extern from *:
5555
"""
5656

5757
cdef extern from "flint/flint.h":
58+
# These defines are needed to work around a Cython bug.
59+
# Otherwise sizeof(ulong) will give the wrong size on 64 bit Windows.
60+
# https://github.com/cython/cython/issues/6339
5861
"""
5962
#define SIZEOF_ULONG sizeof(ulong)
6063
#define SIZEOF_SLONG sizeof(slong)

src/flint/test/test_all.py

+104-3
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

+191-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,19 @@ 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.types.flint cimport SIZEOF_SLONG
13+
14+
from flint.flintlib.functions.fmpz cimport (
15+
fmpz_set,
16+
fmpz_init,
17+
fmpz_clear,
18+
fmpz_set_si,
19+
fmpz_mul,
20+
fmpz_equal,
21+
fmpz_equal_si,
22+
)
1223
from flint.flintlib.functions.fmpz cimport fmpz_is_zero, fmpz_is_pm1
1324
from flint.flintlib.types.fmpz cimport (
1425
fmpz_mat_struct,
@@ -316,6 +327,62 @@ cdef class fmpz_mat(flint_mat):
316327
fmpz_mat_pow(t.val, t.val, ee)
317328
return t
318329

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

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

0 commit comments

Comments
 (0)