Skip to content
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

add a method to test whether a polynomial is symmetric #29553

Closed
videlec opened this issue Apr 23, 2020 · 48 comments
Closed

add a method to test whether a polynomial is symmetric #29553

videlec opened this issue Apr 23, 2020 · 48 comments

Comments

@videlec
Copy link
Contributor

videlec commented Apr 23, 2020

As mentioned in this sage-devel thread it is desirable to have a simple check to test whether a given polynomial is symmetric (with respect to a given permutation group).

This ticket aims to implement a generic is_symmetric(self, group=None) on multivariate polynomials (where the default group is the full symmetric group).

We also update the check of symmetry in SymmetricFunctions so that construction from polynomials get faster. The computation time for

sage: n = 10
sage: f = x / (1-exp(-x))
sage: Sym = SymmetricFunctions(QQ)
sage: P = PolynomialRing(QQ, 'x', n)
sage: seq = prod(f.subs({f.default_variable(): var}) for var in P.gens())
sage: sage: inp = [(var, 0) for var in P.gens()]
sage: seq_taylor = seq.taylor(*inp, n // 2)
sage: Sym.e().from_polynomial(P(seq_taylor))

went down from a minute to half a second.

CC: @nbruin @tscrim

Component: algebra

Author: Vincent Delecroix

Branch/Commit: c53ac9b

Reviewer: Travis Scrimshaw, Markus Wageringel

Issue created by migration from https://trac.sagemath.org/ticket/29553

@videlec videlec added this to the sage-9.1 milestone Apr 23, 2020
@videlec
Copy link
Contributor Author

videlec commented Apr 23, 2020

Commit: 41faa3e

@videlec
Copy link
Contributor Author

videlec commented Apr 23, 2020

Branch: u/vdelecroix/29553

@videlec
Copy link
Contributor Author

videlec commented Apr 23, 2020

Author: Vincent Delecroix

@videlec
Copy link
Contributor Author

videlec commented Apr 23, 2020

New commits:

41faa3eis_symmetric for multivariate polynomials

@sagetrac-git
Copy link
Mannequin

sagetrac-git mannequin commented Apr 23, 2020

Changed commit from 41faa3e to 98db2ca

@sagetrac-git
Copy link
Mannequin

sagetrac-git mannequin commented Apr 23, 2020

Branch pushed to git repo; I updated commit sha1. New commits:

98db2cadocumentation fix

@sagetrac-git
Copy link
Mannequin

sagetrac-git mannequin commented Apr 23, 2020

Branch pushed to git repo; I updated commit sha1. New commits:

7c79429fix doctest
4cabb57speed up check in SymmetricFunctions

@sagetrac-git
Copy link
Mannequin

sagetrac-git mannequin commented Apr 23, 2020

Changed commit from 98db2ca to 4cabb57

@videlec

This comment has been minimized.

@videlec

This comment has been minimized.

@mwageringel
Copy link

comment:6

It might be good to convert the polynomial to a dictionary first in order to speed up the looking up of coefficients, e.g.:

-        for e in self.exponents():
-            coeff = self[e]
-            for g in gens:
-                if self[e.permuted(g)] != coeff:
-                    return False
-        return True
+        pd = self.dict()
+        return all(pd[e.permuted(g)] == c for e, c in pd.items() for g in gens)

For the example at hand, this is noticeably faster:

sage: sage: n = 12
....: sage: f = x / (1-exp(-x))
....: sage: Sym = SymmetricFunctions(QQ)
....: sage: P = PolynomialRing(QQ, 'x', n)
....: sage: seq = prod(f.subs({f.default_variable(): var}) for var in P.gens())
....: sage: sage: inp = [(var, 0) for var in P.gens()]
....: sage: seq_taylor = seq.taylor(*inp, n // 2)
....: sage: g = (P(seq_taylor))
....: %time g.is_symmetric()
....:
CPU times: user 2.48 s, sys: 5.07 ms, total: 2.48 s
Wall time: 2.49 s                                       # before
CPU times: user 121 ms, sys: 2.11 ms, total: 123 ms
Wall time: 123 ms                                       # after
True

and even for small polynomials this seems to be at least as fast:

sage: R.<x,y,z> = QQ[]
....: f = (1 + x^2 + y^2 + z^2)^2
....: %timeit f.is_symmetric()
....:
10000 loops, best of 5: 177 µs per loop    # before
10000 loops, best of 5: 64.3 µs per loop   # after

Secondly, in permgroup_element.pyx, there are methods _act_on_list_on_position and _act_on_array_on_position. Do you think it would be more consistent to add a PermutationGroupElement._act_on_etuple_on_position, rather than ETuple.permuted?

Also, you will want to merge #29540.

@mkoeppe mkoeppe modified the milestones: sage-9.1, sage-9.2 Apr 23, 2020
@videlec
Copy link
Contributor Author

videlec commented Apr 23, 2020

comment:8

Replying to @mwageringel:

It might be good to convert the polynomial to a dictionary first in order to speed up the looking up of coefficients, e.g.:

It is sad this is the case. The polynomial datastructure is supposed to be fast in accessing coefficients... I believe it strongly depends on the base ring. But given the time difference I agree that it makes sense.

<SNIP

Secondly, in permgroup_element.pyx, there are methods _act_on_list_on_position and _act_on_array_on_position. Do you think it would be more consistent to add a PermutationGroupElement._act_on_etuple_on_position, rather than ETuple.permuted?

Sure. That will also be convenient for action of permutations on polynomials.

Also, you will want to merge #29540.

Indeed.

@tscrim
Copy link
Collaborator

tscrim commented Apr 23, 2020

comment:9

Replying to @videlec:

Replying to @mwageringel:

It might be good to convert the polynomial to a dictionary first in order to speed up the looking up of coefficients, e.g.:

It is sad this is the case. The polynomial datastructure is supposed to be fast in accessing coefficients... I believe it strongly depends on the base ring. But given the time difference I agree that it makes sense.

The MPolynomial_libsignular.__getitem__ code looks like this:

        m = p_ISet(1,r)
        i = 1
        for e in x:
            overflow_check(e, r)
            p_SetExp(m, i, int(e), r)
            i += 1
        p_Setm(m, r)

        while(p):
            if p_ExpVectorEqual(p, m, r) == 1:
                p_Delete(&m,r)
                return si2sa(p_GetCoeff(p, r), r, self._parent._base)
            p = pNext(p)

So it looks more like it is going through a list rather than a dict. I don't know how singular does this, but it looks like a very different data structure than the naïve implementation. IIRC, their data structure is solely to be efficient at computing Gröbner bases.

TL;DR Converting to a dict is definitely the best option with the current implementation.

As a more broader question, it might be worthwhile to consider reimplementing generic multivariate polynomials in Cython and only convert to (lib)singular when wanting a Gröbner basis.

@dimpase
Copy link
Member

dimpase commented Apr 24, 2020

comment:10

How about generalising it to computing actions of linear group elements and of linear groups on polynomials?

It seems to be a bit restrictive to only have methods for fixed point computation, whereas it's only slightly less general, and you compute the action anyway!

@videlec
Copy link
Contributor Author

videlec commented Apr 24, 2020

comment:11

Replying to @dimpase:

How about generalising it to computing actions of linear group elements and of linear groups on polynomials?

It seems to be a bit restrictive to only have methods for fixed point computation, whereas it's only slightly less general, and you compute the action anyway!

Feel free to open a ticket for that. The code here is permuting exponents and do not touch the coefficients. The linear action might better be done via a proper action.

@sagetrac-git
Copy link
Mannequin

sagetrac-git mannequin commented Apr 24, 2020

Changed commit from 4cabb57 to 4e3cffe

@sagetrac-git
Copy link
Mannequin

sagetrac-git mannequin commented Apr 24, 2020

Branch pushed to git repo; I updated commit sha1. This was a forced push. New commits:

ccdd618is_symmetric for multivariate polynomials
4e3cffespeed up check in SymmetricFunctions

@videlec
Copy link
Contributor Author

videlec commented Apr 24, 2020

comment:13

Implementation changed to go via action of PermutationGroupElement on ETuple.

@sagetrac-git
Copy link
Mannequin

sagetrac-git mannequin commented Apr 24, 2020

Branch pushed to git repo; I updated commit sha1. New commits:

1470131don't call exponents + force conversion to SymmetricGroup(n)

@sagetrac-git
Copy link
Mannequin

sagetrac-git mannequin commented Apr 24, 2020

Changed commit from 4e3cffe to 1470131

@dimpase
Copy link
Member

dimpase commented Apr 25, 2020

comment:15

Does it work for Laurent polynomials?

@dimpase
Copy link
Member

dimpase commented Apr 25, 2020

comment:16
result._data = <int*> sig_malloc(sizeof(int)*result._nonzero*2)

looks like a memory leak, as I don't see a matching sig_free() call.

@videlec
Copy link
Contributor Author

videlec commented Apr 25, 2020

comment:21

Replying to @tscrim:

Replying to @dimpase:

result._data = <int*> sig_malloc(sizeof(int)*result._nonzero*2)

looks like a memory leak, as I don't see a matching sig_free() call.

Why would there be a sig_free()? result is the return value of the function, and it would be the job of the ETuple to handle freeing that in its deallocation I believe.

Indeed. That is exactly what is happening.

@sagetrac-git
Copy link
Mannequin

sagetrac-git mannequin commented Apr 25, 2020

Branch pushed to git repo; I updated commit sha1. New commits:

0d72df3some details

@sagetrac-git
Copy link
Mannequin

sagetrac-git mannequin commented Apr 25, 2020

Changed commit from 1470131 to 0d72df3

@fchapoton
Copy link
Contributor

comment:23

beware that monomial.py has been modified recently, in #29540

@mwageringel
Copy link

comment:24

To me the naming "on position" suggests a certain way a permutation acts on lists, independently from the notion of left and right (although this action does satisfy the properties of a left action). So I think either the _on_position or the self_on_left should be removed, as these terms are at odds with each other.

I would probably drop the self_on_left as it is simple enough to call the method on the inverse permutation instead when necessary, but your current implementation does the opposite of what _act_on_list_on_position does. I had not considered this before - sorry. Since you mentioned the action on polynomials, I can see why the current implementation might be preferable. In that case, you could just rename the method to _act_on_etuple.

In any case, left and right seem to be mixed up currently:

sage: S = SymmetricGroup(6)
sage: p, q = S('(1,2,3,4,5,6)'), S('(1,2)(3,4)(5,6)')
sage: from sage.rings.polynomial.polydict import ETuple
sage: e = ETuple([10..15])
sage: right = lambda x, p: p._act_on_etuple_on_position(x, self_on_left=False)
sage: right(e, p * q) == right(right(e, p), q)  # should be True
False

@mwageringel
Copy link

comment:25

Replying to @tscrim:

As a more broader question, it might be worthwhile to consider reimplementing generic multivariate polynomials in Cython and only convert to (lib)singular when wanting a Gröbner basis.

This is exactly what the polydict implementation of polynomials does, no? You can construct a polynomial ring via PolynomialRing(..., implementation='generic') to use it, and when you want a Gröbner basis, only then it will convert to Singular (the conversion to libsingular does not seem to be supported).

On the other hand, I cannot think of many operations on polynomials for which random access to arbitrary coefficients is important.

@videlec
Copy link
Contributor Author

videlec commented Apr 25, 2020

comment:26

Replying to @mwageringel:

To me the naming "on position" suggests a certain way a permutation acts on lists, independently from the notion of left and right (although this action does satisfy the properties of a left action). So I think either the _on_position or the self_on_left should be removed, as these terms are at odds with each other.

I would probably drop the self_on_left as it is simple enough to call the method on the inverse permutation instead when necessary, but your current implementation does the opposite of what _act_on_list_on_position does. I had not considered this before - sorry. Since you mentioned the action on polynomials, I can see why the current implementation might be preferable. In that case, you could just rename the method to _act_on_etuple.

In any case, left and right seem to be mixed up currently:

sage: S = SymmetricGroup(6)
sage: p, q = S('(1,2,3,4,5,6)'), S('(1,2)(3,4)(5,6)')
sage: from sage.rings.polynomial.polydict import ETuple
sage: e = ETuple([10..15])
sage: right = lambda x, p: p._act_on_etuple_on_position(x, self_on_left=False)
sage: right(e, p * q) == right(right(e, p), q)  # should be True
False

I just copied what is in _act_on_list_on_position which claims to be a right action. Same behaviour

sage: S = SymmetricGroup(6)
sage: p, q = S('(1,2,3,4,5,6)'), S('(1,2)(3,4)(5,6)')
sage: right = lambda x,p: p._act_on_list_on_position(x)
sage: e = [10..15]
sage: right(e, p * q) == right(right(e, p), q)
False
sage: right(e, p * q) == right(right(e, q), p)
True

@sagetrac-git
Copy link
Mannequin

sagetrac-git mannequin commented Apr 25, 2020

Changed commit from 0d72df3 to 989716c

@sagetrac-git
Copy link
Mannequin

sagetrac-git mannequin commented Apr 25, 2020

Branch pushed to git repo; I updated commit sha1. New commits:

6f299fadrop self_on_left
e119938spring cleanup in sf/monomial (pep, code, doc details)
9570e17trac 29540 still better code in monomial.py, other details there
f7381eatrac 29540 fix import of _Partitions
989716cMerge changes from ticket #29540

@mwageringel
Copy link

comment:28

Replying to @videlec:

I just copied what is in _act_on_list_on_position which claims to be a right action. Same behaviour

The action of permutations on matrices implemented in _act_on_ also has it the other way around, but for polynomials it works as expected:

sage: S = SymmetricGroup(6)
sage: p, q = S('(1,2,3,4,5,6)'), S('(1,2)(3,4)(5,6)')
sage: M = matrix.diagonal([1..6])
sage: (M * p) * q == M * (p * q)
False
sage: R = PolynomialRing(QQ, 'x', 6)
sage: (R.0 * p) * q == R.0 * (p * q)
True

Am I misunderstanding something about left and right actions in Sage?

@tscrim
Copy link
Collaborator

tscrim commented Apr 25, 2020

comment:29

Replying to @mwageringel:

Replying to @tscrim:

As a more broader question, it might be worthwhile to consider reimplementing generic multivariate polynomials in Cython and only convert to (lib)singular when wanting a Gröbner basis.

This is exactly what the polydict implementation of polynomials does, no? You can construct a polynomial ring via PolynomialRing(..., implementation='generic') to use it, and when you want a Gröbner basis, only then it will convert to Singular (the conversion to libsingular does not seem to be supported).

Sorry, I forgot generic is an overloaded word here and probably not the best word. What I meant was more universally shall we do this for rings that can be converted to (lib)singular, like those over Z.

On the other hand, I cannot think of many operations on polynomials for which random access to arbitrary coefficients is important.

Perhaps you're right. I can think of a number of things where you want to iterate over the pairs of coefficients and exponents. I don't think we have a method to do that (the default iterator is quite bad, getting the list of coefficients and list of monomials and zipping them together). I will open a ticket tomorrow or the next day to try and improve the iteration.

@tscrim
Copy link
Collaborator

tscrim commented Apr 25, 2020

comment:30

Replying to @mwageringel:

The action of permutations on matrices implemented in _act_on_ also has it the other way around, but for polynomials it works as expected:

sage: S = SymmetricGroup(6)
sage: p, q = S('(1,2,3,4,5,6)'), S('(1,2)(3,4)(5,6)')
sage: M = matrix.diagonal([1..6])
sage: (M * p) * q == M * (p * q)
False
sage: R = PolynomialRing(QQ, 'x', 6)
sage: (R.0 * p) * q == R.0 * (p * q)
True

Am I misunderstanding something about left and right actions in Sage?

Here is where it comes from I think:

sage: (M * p.matrix()) * q.matrix() == M * (p.matrix() * q.matrix())
True
sage: (M * p.matrix()) * q.matrix() == M * (p*q).matrix()
True
sage: p.matrix() * q.matrix() == (p*q).matrix()
True
sage: p.matrix() * M == M * p
True

(An oddity that needs fixing: p.matrix() works but p.to_matrix() is a NotImplemented.)
Also Sage only knows it has a right action:

sage: (q * p) * M == q * (p * M)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<snip>
TypeError: unsupported operand parent(s) for *: 'Symmetric group of order 6! as a permutation group' and 'Full MatrixSpace of 6 by 6 sparse matrices over Integer Ring'

So I think the thing that needs to change in the perm group element _act_on_ is

             elif is_Matrix(left):
+               return left.with_permuted_columns(~self)
+        else:
+            if is_Matrix(left):
                return left.with_permuted_rows(self)

Addendum: Because of this:

sage: M * p.matrix()
[0 1 0 0 0 0]
[0 0 2 0 0 0]
[0 0 0 3 0 0]
[0 0 0 0 4 0]
[0 0 0 0 0 5]
[6 0 0 0 0 0]
sage: M.with_permuted_columns(~p)
[0 1 0 0 0 0]
[0 0 2 0 0 0]
[0 0 0 3 0 0]
[0 0 0 0 4 0]
[0 0 0 0 0 5]
[6 0 0 0 0 0]

@mwageringel
Copy link

Reviewer: Travis Scrimshaw, Markus Wageringel

@mwageringel
Copy link

comment:31

Replying to @tscrim:

Sorry, I forgot generic is an overloaded word here and probably not the best word. What I meant was more universally shall we do this for rings that can be converted to (lib)singular, like those over Z.

So you are suggesting to make the generic implementation the default? This implementation is not usually faster than the Singular backend I think, but it depends on the use case of course.

Perhaps you're right. I can think of a number of things where you want to iterate over the pairs of coefficients and exponents. I don't think we have a method to do that (the default iterator is quite bad, getting the list of coefficients and list of monomials and zipping them together). I will open a ticket tomorrow or the next day to try and improve the iteration.

That would indeed be nice to have, as it is such a common operation. I did not know the __iter__ method was implemented for polynomials and have always been zipping coefficients and monomials (or exponents), but it always felt odd to me, especially since this pattern fails for CombinatorialFreeModule elements if one does not pay attention to the sorting.

Regarding the action on matrices, we could handle that on a new ticket, as it alters existing behavior and is not really related to the aim of this ticket.

I am happy with this ticket as it is now. Except maybe there is one little detail from Travis' suggestion:

        out = self._from_dict({_Partitions.element_class(_Partitions, list(e)): R(c)
                               for (e,c) in f.dict().items()

The conversion R(c) should not be necessary here, as the coefficients should already be elements of the base ring.

@mwageringel
Copy link

comment:32

Replying to @tscrim:

(An oddity that needs fixing: p.matrix() works but p.to_matrix() is a NotImplemented.)

Do you know where this to_matrix comes from? I stumbled upon this a few days ago, but could not figure out where it was defined.

@sagetrac-git
Copy link
Mannequin

sagetrac-git mannequin commented Apr 26, 2020

Changed commit from 989716c to c53ac9b

@sagetrac-git
Copy link
Mannequin

sagetrac-git mannequin commented Apr 26, 2020

Branch pushed to git repo; I updated commit sha1. New commits:

c53ac9bdon't convert since base ring is already correct

@tscrim
Copy link
Collaborator

tscrim commented Apr 27, 2020

comment:34

Replying to @mwageringel:

Replying to @tscrim:

Sorry, I forgot generic is an overloaded word here and probably not the best word. What I meant was more universally shall we do this for rings that can be converted to (lib)singular, like those over Z.

So you are suggesting to make the generic implementation the default? This implementation is not usually faster than the Singular backend I think, but it depends on the use case of course.

Yes, although I am not sure if this will be a good option. However, IIRC things like multiplying polynomials is really slow and could use another library to speed that up.

Perhaps you're right. I can think of a number of things where you want to iterate over the pairs of coefficients and exponents. I don't think we have a method to do that (the default iterator is quite bad, getting the list of coefficients and list of monomials and zipping them together). I will open a ticket tomorrow or the next day to try and improve the iteration.

That would indeed be nice to have, as it is such a common operation. I did not know the __iter__ method was implemented for polynomials and have always been zipping coefficients and monomials (or exponents), but it always felt odd to me, especially since this pattern fails for CombinatorialFreeModule elements if one does not pay attention to the sorting.

This is now #29595.

Regarding the action on matrices, we could handle that on a new ticket, as it alters existing behavior and is not really related to the aim of this ticket.

I agree that it should be a separate ticket.

I am happy with this ticket as it is now. Except maybe there is one little detail from Travis' suggestion:

        out = self._from_dict({_Partitions.element_class(_Partitions, list(e)): R(c)
                               for (e,c) in f.dict().items()

The conversion R(c) should not be necessary here, as the coefficients should already be elements of the base ring.

If we add that conversion, then we can remove the

assert self.base_ring() == f.base_ring()

which I think is not a good thing to enforce.

@tscrim
Copy link
Collaborator

tscrim commented Apr 27, 2020

comment:35

Replying to @mwageringel:

Replying to @tscrim:

(An oddity that needs fixing: p.matrix() works but p.to_matrix() is a NotImplemented.)

Do you know where this to_matrix comes from? I stumbled upon this a few days ago, but could not figure out where it was defined.

It comes from the finite Complex reflection group category. This can be easily fixed with an alias in the (finite) Coxeter group category (using the method canonical_matrix) and/or for the specific implementation of permutation groups.

@videlec
Copy link
Contributor Author

videlec commented Apr 27, 2020

comment:36

Replying to @tscrim:

Replying to @mwageringel:

Replying to @tscrim:
I am happy with this ticket as it is now. Except maybe there is one little detail from Travis' suggestion:

        out = self._from_dict({_Partitions.element_class(_Partitions, list(e)): R(c)
                               for (e,c) in f.dict().items()

The conversion R(c) should not be necessary here, as the coefficients should already be elements of the base ring.

If we add that conversion, then we can remove the

assert self.base_ring() == f.base_ring()

which I think is not a good thing to enforce.

Indeed, I found this line a bit weird. Though I did not touch since it was beyond the scope of the ticket.

@mwageringel
Copy link

comment:37

Ok, it seems this is ready to be merged then? Let me set this ticket to positive, but please undo if you disagree.

@vbraun
Copy link
Member

vbraun commented May 4, 2020

Changed branch from u/vdelecroix/29553 to c53ac9b

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants