New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
matrices and groups: Smith Normal Form and an infinity test for fp groups #12705
Changes from 8 commits
ce90e35
64bdba5
bbd188f
479f1f0
ac8f255
2fbe92b
a012c58
1f2111b
3832a8b
eba80ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
from __future__ import print_function, division | ||
from sympy.matrices import diag | ||
|
||
''' | ||
Functions returning normal forms of matrices | ||
|
||
''' | ||
def smith_normal_form(m, domain = None): | ||
''' | ||
Return the Smith Normal Form of a matrix `m` over the ring `domain`. | ||
This will only work if the ring is a principal ideal domain. | ||
|
||
Examples | ||
======== | ||
|
||
>>> from sympy.polys.solvers import RawMatrix as Matrix | ||
>>> from sympy.polys.domains import ZZ | ||
>>> from sympy.matrices.normalforms import smith_normal_form | ||
>>> m = Matrix([[12, 6, 4], [3, 9, 6], [2, 16, 14]]) | ||
>>> setattr(m, "ring", ZZ) | ||
>>> print(smith_normal_form(m)) | ||
Matrix([[1, 0, 0], [0, 10, 0], [0, 0, -30]]) | ||
|
||
''' | ||
invs = smith_normal_invariants(m, domain=domain) | ||
smf = diag(*invs) | ||
n = len(invs) | ||
if m.rows > n: | ||
smf = smf.row_insert(m.rows, zeros(m.rows-n, m.cols)) | ||
elif m.cols > n: | ||
smf = smf.col_insert(m.cols, zeros(m.rows, m.cols-n)) | ||
return smf | ||
|
||
def smith_normal_invariants(m, domain = None): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe this could be |
||
''' | ||
Return the tuple of abelian invariants for a matrix `m` | ||
(as in the Smith-Normal form) | ||
|
||
References | ||
========== | ||
|
||
[1] https://en.wikipedia.org/wiki/Smith_normal_form#Algorithm | ||
[2] http://sierra.nmsu.edu/morandi/notes/SmithNormalForm.pdf | ||
|
||
''' | ||
if not domain: | ||
if not (hasattr(m, "ring") and m.ring.is_PID): | ||
raise ValueError( | ||
"The matrix entries must be over a principal ideal domain") | ||
else: | ||
domain = m.ring | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should check here that the matrix is not empty. If it is (i.e., |
||
if len(m) == 0: | ||
return () | ||
|
||
m = m[:, :] | ||
|
||
def add_rows(m, i, j, a, b, c, d): | ||
# replace m[i, :] by a*m[i, :] + b*m[j, :] | ||
# and m[j, :] by c*m[i, :] + d*m[j, :] | ||
for k in range(m.cols): | ||
e = m[i, k] | ||
m[i, k] = a*e + b*m[j, k] | ||
m[j, k] = c*e + d*m[j, k] | ||
|
||
def add_columns(m, i, j, a, b, c, d): | ||
# replace m[:, i] by a*m[:, i] + b*m[:, j] | ||
# and m[:, j] by c*m[:, i] + d*m[:, j] | ||
for k in range(m.rows): | ||
e = m[k, i] | ||
m[k, i] = a*e + b*m[k, j] | ||
m[k, j] = c*e + d*m[k, j] | ||
|
||
def clear_column(m): | ||
# make m[1:, 0] zero by row and column operations | ||
if m[0,0] == 0: | ||
return m | ||
pivot = m[0, 0] | ||
for j in range(1, m.rows): | ||
if m[j, 0] == 0: | ||
continue | ||
d, r = domain.div(m[j,0], pivot) | ||
if r == 0: | ||
add_rows(m, 0, j, 1, 0, -d, 1) | ||
else: | ||
a, b, g = domain.gcdex(pivot, m[j,0]) | ||
d_0 = domain.div(m[j, 0], g)[0] | ||
d_j = domain.div(pivot, g)[0] | ||
add_rows(m, 0, j, a, b, d_0, -d_j) | ||
pivot = g | ||
return m | ||
|
||
def clear_row(m): | ||
# make m[0, 1:] zero by row and column operations | ||
if m[0] == 0: | ||
return m | ||
pivot = m[0, 0] | ||
for j in range(1, m.cols): | ||
if m[0, j] == 0: | ||
continue | ||
d, r = domain.div(m[0, j], pivot) | ||
if r == 0: | ||
add_columns(m, 0, j, 1, 0, -d, 1) | ||
else: | ||
a, b, g = domain.gcdex(pivot, m[0, j]) | ||
d_0 = domain.div(m[0, j], g)[0] | ||
d_j = domain.div(pivot, g)[0] | ||
add_columns(m, 0, j, a, b, d_0, -d_j) | ||
pivot = g | ||
return m | ||
|
||
# permute the rows and columns until m[0,0] is non-zero if possible | ||
ind = [i for i in range(m.rows) if m[i,0] != 0] | ||
if ind: | ||
m = m.permute_rows([[0, ind[0]]]) | ||
else: | ||
ind = [j for j in range(m.cols) if m[0,j] != 0] | ||
if ind: | ||
m = m.permute_cols([[0, ind[0]]]) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Else both the first column and the first row consist of zeros and we have to scan the whole matrix. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If both the first column and row are zero, the function will just add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should find the invariants in (divisibility) order starting with the gcd of all matrix entries and leaving the zeros, if any, to the end. Otherwise expressed, we should use the inverted inclusion order of the ideals generated by the invariants, first the largest one, generated by all matrix entries, and the smallest ideals, This means that we should add 0 to the invariant list only when there are no nonzero entries left. Therefore all entries should be scanned at this point. |
||
# make the first row and column except m[0,0] zero | ||
while (any([m[0,i] != 0 for i in range(1,m.cols)]) or | ||
any([m[i,0] != 0 for i in range(1,m.rows)])): | ||
m = clear_column(m) | ||
m = clear_row(m) | ||
|
||
if 1 in m.shape: | ||
invs = () | ||
else: | ||
invs = smith_normal_invariants(m[1:,1:], domain=domain) | ||
|
||
if m[0,0]: | ||
result = [m[0,0]] | ||
result.extend(invs) | ||
# in case m[0] doesn't divide the invariants of the rest of the matrix | ||
for i in range(len(result)-1): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is not clear to me that this loop would give the correct invariants. Can you give some proof? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My reasoning was like this. Add (i+1)-th row to i-th row, postmultiply by the matrix that in the 2x2 case looks like As for divisibility, the idea is this. We've just found that the gcd of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the explanation, that is about what I was expecting. It seems that it is valid though I don't recall having seen that algorithm explained anywhere. Can you find a reference? There should be references for algorithms used in SymPy, especially for those that are not as familiar as Euclid's algorithm. Othewise there should be adequate comments along the code, which at this point might become rather longish. I'd also suggest that that the zero invariants would come last. (That would be natural as I noted above.) Then it seems that it would not be necessary to scan There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it would be more natural and I had zeros at the end initially. I introduced the scanning of I'll try to search for a reference. Most of what I did is described on wikipedia but the last bit I worked out on paper. I'd be surprised if this shortcut of the last step isn't mentioned anywhere though. |
||
if result[i] and domain.div(result[i+1], result[i])[1] != 0: | ||
g = domain.gcd(result[i+1], result[i]) | ||
result[i+1] = domain.div(result[i], g)[0]*result[i+1] | ||
result[i] = g | ||
else: | ||
break | ||
else: | ||
result = invs + (m[0,0],) | ||
return tuple(result) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
from __future__ import print_function, division | ||
|
||
from sympy import Symbol, Poly | ||
from sympy.polys.solvers import RawMatrix as Matrix | ||
from sympy.matrices.normalforms import smith_normal_invariants, smith_normal_form | ||
from sympy.polys.domains import ZZ, QQ | ||
|
||
def test_smith_normal(): | ||
m = Matrix([[12, 6, 4,8],[3,9,6,12],[2,16,14,28],[20,10,10,20]]) | ||
setattr(m, 'ring', ZZ) | ||
smf = Matrix([[1, 0, 0, 0], [0, 10, 0, 0], [0, 0, -30, 0], [0, 0, 0, 0]]) | ||
assert smith_normal_form(m) == smf | ||
|
||
x = Symbol('x') | ||
m = Matrix([[Poly(x-1), Poly(1, x),Poly(-1,x)], | ||
[0, Poly(x), Poly(-1,x)], | ||
[Poly(0,x),Poly(-1,x),Poly(x)]]) | ||
setattr(m, 'ring', QQ[x]) | ||
invs = (Poly(1, x), Poly(x - 1), Poly(x**2 - 1)) | ||
assert smith_normal_invariants(m) == invs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a docstring for this function along with information regarding type of inputs and returned values?
Few examples to go along with this would be ideal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should also be some reference though there are not many apart from Wikipedia. That will contain further references to the theoretical background (Lang's Algebra, 3rd ed.) and applications to modules over PIDs (invariant factors, elementary divisors).