Skip to content

functorial composition #40186

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

Open
wants to merge 157 commits into
base: develop
Choose a base branch
from

Conversation

mantepse
Copy link
Contributor

@mantepse mantepse commented May 30, 2025

Provide an implementation of functorial composition.

This is the first implementation that can do the molecular expansion. The algorithm is entirely naive, except for using gap to the best of my knowledge. Thus, even rather small examples cannot be computed. I doubt that a significantly better algorithm exists, however.

Dependencies: #38544

@DaveWitteMorris
Copy link
Member

It's great if GAP actions will solve the problem, but I don't know anything about them. Here is a sketch of a different method I think you could use to have sagemath and GAP do the calculation if the stabilizers are small, meaning that they each have only a few dozen subgroups.

A group G is acting transitively on a set X, with stabilizer G_x, and we have a subgroup H of G. For each subgroup H' of H, here is a way to calculate the number of points x in X, such that H' is the stabilizer of x in the subgroup H. (Dividing this number by |H/H'| then tells you the coefficient of the action H/H' in the restriction to H.)

For simplicity, let's first take the case where H' = H (so we are looking for the number of fixed points of H). Let n_0 be the number of subgroups of G_x that are conjugate to H'. (For each subgroup K of G_x, GAP can check whether H' is conjugate to K. If it is, then |N_G(H')| is the number of elements of G that conjugate H' to K, where N_G(H') is the normalizer of H' in G.) Then
the number of fixed points is n_0 |N_G(H')| / |G_x|.

The next step is to look at the case where H' is a maximal subgroup of H. Let n_1 be the number of subgroups of G_x that are conjugate to H'. The number of elements of G that conjugate H' into a subgroup of G_x is n_1 |N_G(H')|. However, some of these elements conjugate all of H into G_x, and therefore correspond to fixed points of the action of H, rather than orbits isomorphic to H/H'. Therefore,
the number of copies of H/H' is (n_1 - n_0) |N_G(H')| / (|G_x| |H/H'|)

Continuing in this way (essentially using inclusion-exclusion) to subtract off elements of G that give smaller orbits, sage can find the coefficient of H/H' for every subgroup H' of H. (Only take one representative of each conjugacy class of subgroups of H, though.)

@dimpase
Copy link
Member

dimpase commented Jun 19, 2025

this seems like a huge overkill - what's unclear about GAP's actions? An action on a domain is specified by a function a(g,x) which takes a group element g and a domain element x, and computes g(x).

There are also few functions which actually don't adhere to this generic standard, but compute actions directly, without using a(g,x), e.g. the one for actions on cosets of a subgroup

@DaveWitteMorris
Copy link
Member

I know almost nothing about GAP actions, so I don't know whether they can do the necessary calculations (such as finding the number of points that have a given stabilizer) for an action on a set with too many elements to list.

@dimpase
Copy link
Member

dimpase commented Jun 19, 2025

finding the pointwise stabiliser of a subset of the domain in a permutation action is a standard thing in algorithms for permutation groups (involving "bases"- minimal wrt inclusion subsets B={B_1,..., B_k} fixed by every element, and "strong stabiliser chains" - subgroups G_j fixing B_1,...B_j, for all 0<j<k)

@DaveWitteMorris
Copy link
Member

I don't think that's what we're looking for here. Basically, we're given a subgroup of the acting group (not a subset of the domain), and we want to know how many points it fixes. (More precisely, we want to know how many points have it as the exact stabilizer.) My understanding is that there are too many points (say 10^30) to actually find the fixed points, so we just want to know how many there are.

@DaveWitteMorris
Copy link
Member

There are also few functions which actually don't adhere to this generic standard, but compute actions directly, without using a(g,x), e.g. the one for actions on cosets of a subgroup

An action on G/H is what we're looking at. For K_1 < K < G, we want to know how many points of G/H have K_1 as the stabilizer for the action of K on G/H.

@dimpase
Copy link
Member

dimpase commented Jun 19, 2025

OK, then it's probably already in GAP - tables of marks: https://docs.gap-system.org/doc/ref/chap70.html

@DaveWitteMorris
Copy link
Member

I think G might be too big to calculate the subgroup lattice, say S_20 or S_30, so we can't get its table of marks. But H and K are small enough (maybe S_4 or S_5) that we can compute their subgroup lattices. It would be great if GAP could compute the conjugacy information just for subgroups of H and K. (That's what I was trying to do by hand in my first answer.)

@dimpase
Copy link
Member

dimpase commented Jun 20, 2025 via email

@mantepse
Copy link
Contributor Author

I don't think that's what we're looking for here. Basically, we're given a subgroup of the acting group (not a subset of the domain), and we want to know how many points it fixes. (More precisely, we want to know how many points have it as the exact stabilizer.) My understanding is that there are too many points (say 10^30) to actually find the fixed points, so we just want to know how many there are.

I am very interested, but I haven't digested your idea completely yet. Do you think you could walk me through? For example, following my notation from the sage-devel post:

  • if F is the action whose stabilizer is the cyclic group generated by (1,2,3,4,5,6) and G is the action with trivial stabilizer group as a subgroup of S_3, we should get

    • 3 times the stabilizer group (1,2,3)
    • 8 times the stabilizer group generated by (1,2)
    • 15 times the trivial stabilizer.
  • if F is the action with stabilizer generated by (1,2,3,4,5,6,7,8) and G is the action with one stabilizer subgroup being trivial as subgroup of S_3 and another generated by (1,2,3) we should get

    • 48 times the stabilizer group generated by (1,2)
    • 816 times the trivial stabilizer
  • if F is the action with stabilizer generated by (1,2,...,24) and G is the action with trivial stabilizer as a subgroup of S_4, I cannot compute the stabilizers anymore. The size of the set S_4 is acting on is 25852016738884976640000 = 23!.

@DaveWitteMorris
Copy link
Member

@dimpase: I think most of your suggestions would improve both the efficiency and the simplicity of the existing code, but the improvements would be somewhat incremental, because they will rely on making a list of the orbits, and then counting the number of orbits. (That is what the code currently does.) The OP wants a way to calculate the number of orbits (actually, the number of orbits with each possible stabilizer) when there are too many of them to make a list. E.g., he gives an example where the number of orbits is on the order of 23!. My comment above makes the simple observation that this counting is not difficult to do if there are only a few possibilities for the stabilizers. We do some small calculations, and then multiply by |N_G(H')| (which may be huge) to get the number of orbits of a given type.

@mantepse: I will try to respond soon with a more concrete explanation of how to use my suggestion to do the calculations for the examples you mentioned. It should not be difficult to automate in sagemath (using GAP), but may require some thought to get the inclusion-exclusion right.

@dimpase
Copy link
Member

dimpase commented Jun 20, 2025

The number of orbits can be computed from the character of the action, which is often easier to compute. It's what's known as "orbit counting lemma", attributed to Burnside: the number of orbits is the average of the number of fixed points for each group element. I have it in my old bloggified lecture notes: https://equatorialmaths.wordpress.com/2009/03/31/orbit-counting-lemma/

@DaveWitteMorris
Copy link
Member

Before doing specific examples, let me explain the general method more clearly. I am explaining a simple method that will work. It can presumably be made more efficient by doing some optimizations. (For example, if two subgroups of H are conjugate in G, then they have the same number of fixed points, so there is no need to do the calculations for both subgroups.)

We are given a permutation group G. (In the examples, G is S_N.) We are also given subgroups G_x and H of G, and we are interested in the action of H on the coset space G/G_x. (To do calculations in GAP, the subgroups should be given by generators that are permutations, and the generators must be elements of G.) For each subgroup H' of H, we wish to calculate the number of H-orbits that have H' as the stabilizer of a point. Let's call this number n(H').

It is easier to calculate a slightly different number which I will call m(H'). This is the number of points whose stabilizer is H'. In H/H', there are exactly |N_H(H')|/|H'| points whose stabilizer is H', so

    n(H') = m(H') |H'| / |N_H(H')|.

To calculate m(H'), we first find the number of fixed points of H' (for every subgroup H'). Then it is easy to calculate m(H') by induction, starting from the top. The key point is just to notice that if x is a fixed point of H', then either the stabilizer of x is H', or the stabilizer of x is some subgroup of H that properly contains H'. Therefore:

   m(H') = f(H') - sum_{H'' > H'} m(H'').

Note that m(H) = f(H). This is the base case of the induction.

Here is how we find f(H'). A coset g G_x is a fixed point of H' if and only if g conjugates H' into G_x. For each subgroup K of G_x, we use GAP to check whether H' is conjugate to K. If it is, then the number of elements of G that conjugate H' to K is |N_G(H')|. Therefore, if we let k(H') be the number of subgroups of G_x that are conjugate to H', then

    f(H') = k(H') |N_G(H')| / |G_x|.

You may have noticed that I did not mention "inclusion-exclusion" in this explanation. That's because it is not needed. I hadn't thought though the argument carefully, and was therefore a bit confused when I gave my previous explanations.

@DaveWitteMorris
Copy link
Member

DaveWitteMorris commented Jun 20, 2025

Example. F is the action whose stabilizer is the cyclic group generated by (1,2,3,4,5,6) and G is the action with trivial stabilizer group as a subgroup of S_3.

We have G = S_6, G_x = < (1,2,3,4,5,6) >, and H is the regular representation of S_3. More precisely,
H = < (1,2,3)(4,5,6), (1,6)(2,5)(3,4) >.

We can do the calculations by hand.

Every point is fixed by the trivial group, so f(<1>) = 5! = 120.

The other easy case is H = S_3. It is nonabelian, so it is not conjugate to any subgroup of G_x. Therefore f(S_3) = 0.

Since G_x is cyclic, it has a unique subgroup of order 2, and this subgroup (1,4)(2,5)(3,6) is indeed conjugate to a subgroup of H. Therefore

    f(H_2) = |N_G(H_2)| / |G_x| whenever H_2 is a subgroup of order 2.  

We know that |G_x| = 6. GAP can calculate |N_G(H_2)|, but we do not need a slegehammer to see that the order is 6 x 2^3 = 48. (Since |H_2| = 2, the normalizer is the same as the centralizer. We can permute the three blocks of size 2 in 3! = 6 ways, and we can interchange the elements of each block.) Therefore

    f(H_2) = 48/6 = 8 when |H_2| = 2.

All that remains is the subgroup of order 3. Again, it is conjugate to the unique subgroup of order 3 in G_x, so

    f(H_3) = |N_G(H_3)| / |G_x| whenever H_3 is a subgroup of order 3.  

For a larger example, we would use GAP to calculate |N_G(H_3)|, but we can do it by hand again in this case. The order of the normalizer is twice the order of the centralizer. An element of the centralizer can interchange the two blocks of size three (which gives another factor of 2), but can only act by a rotation on each block. So

    |N_G(H_3)| = 2 x 2 x 3^2 = 36.

Hence

    f(H_3) = 36 / 6 = 6 when |H_3| = 3.

Now:

    m(H) = f(H) = 0.
    m(H_2) = f(H_2) - m(H) = 8 - 0 = 8.
    m(H_3) = f(H_3) - m(H) = 6 - 0 = 6.

The trivial group is contained in 3 subgroups of order 2 and one subgroup of order 3, so

    m(<1>) = f(<1>) - 3 m(H_2) - m(H_3) = 120 - 3 x 8 - 6 = 90.

Finally:

    n(H) = m(H)/1 = 0
    n(H_2) = m(H_2) |H_2| / |N_H(H_2)| = 8 x 2 / 2 = 8.
    n(H_3) = m(H_3) |H_3| / |N_H(H_3)| = 6 x 3 / 6 = 3.
    n(<1>) = m(<1>) |<1>| / |N_H(<1>)| = 90 x 1 / 6 = 15.

This agrees with your answers.

Is this enough that you can see how to do the other examples (by using GAP when the calculations get large)? You can automate this by having sagemath loop through all of the subgroups of H and G_x.

@mantepse
Copy link
Contributor Author

mantepse commented Jun 20, 2025

Thank you so much! I will check precisely tomorrow! However, 15 is not a typo: we have 5! = 15 * 6 + 8 * 3 + 3 * 2, where 6, 3 and 2 are 6 divided by the sizes of the stabilizer groups.

@DaveWitteMorris
Copy link
Member

Oh, right! I said "Every point is fixed by the trivial group, so f(<1>) = 6! = 720.", but we are acting on 5! points, not 6!, so f(<1>) = 120. I will edit to fix this.

@DaveWitteMorris
Copy link
Member

BTW, it would not be difficult to do all 3 of your examples by hand, like I did the first one. To do the 3rd one, note that you only need to consider subgroups of H that are cyclic (since no other subgroups can be conjugate to a subgroup of G_x), and there are very few of those (up to conjugacy). The normalizers have very large order, but the order is easy to compute.

@mantepse
Copy link
Contributor Author

mantepse commented Jun 21, 2025

I'm currently trying to figure out what H is precisely, given the species G.

EDIT: ah, got it.

EDIT: I have now a very rudimentary version, that seems to give correct results. It doesn't yet work for non-trivial examples, because of a few silly things, that can certainly be avoided (such as computing AllSubgroups). Comments very welcome!

def orbits_with_stabilizer(G, G_x, H, H1, verbose=False):
    """

    EXAMPLES:

    Functorial composition of C_6 and X^3::

        sage: G = SymmetricGroup(6).gap()
        sage: G_x = CyclicPermutationGroup(6).gap()
        sage: g_act = libgap.FactorCosetAction(SymmetricGroup(3), SymmetricGroup(1))
        sage: H = libgap.Group(libgap.MappingGeneratorsImages(g_act)[1])
        sage: H1 = SymmetricGroup(3).gap()
        sage: fixed_points(G, G_x, H1)
        0
        sage: H1 = SymmetricGroup(1).gap()
        sage: fixed_points(G, G_x, H1)
        120
        sage: l_H = H.ConjugacyClassesSubgroups()
        sage: [(H1, fixed_points(G, G_x, H1.Representative()), False) for H1 in l_H]
        [(Group( () )^G, 120),
         (Group( [ (1,4)(2,3)(5,6) ] )^G, 8),
         (Group( [ (1,3,5)(2,4,6) ] )^G, 6),
         (Group( [ (1,3,5)(2,4,6), (1,4)(2,3)(5,6) ] )^G, 0)]

        sage: [(H1, orbits_with_stabilizer(G, G_x, H, H1.Representative())) for H1 in l_H]
        [(Group( () )^G, 15),
         (Group( [ (1,4)(2,3)(5,6) ] )^G, 8),
         (Group( [ (1,3,5)(2,4,6) ] )^G, 3),
         (Group( [ (1,3,5)(2,4,6), (1,4)(2,3)(5,6) ] )^G, 0)]

    Functorial composition of C_6 and C_4::

        sage: G = SymmetricGroup(6).gap()
        sage: G_x = CyclicPermutationGroup(6).gap()
        sage: g_act = libgap.FactorCosetAction(SymmetricGroup(4), CyclicPermutationGroup(4))
        sage: H = libgap.Group(libgap.MappingGeneratorsImages(g_act)[1])
        sage: l_H = H.ConjugacyClassesSubgroups()
        sage: [(H1, orbits_with_stabilizer(G, G_x, H, H1.Representative())) for H1 in l_H]
        [(Group( () )^G, 2),
         (Group( [ (2,4)(3,5) ] )^G, 0),
         (Group( [ (1,3)(2,4)(5,6) ] )^G, 4),
         (Group( [ (1,3,2)(4,6,5) ] )^G, 3),
         (Group( [ (1,6)(3,5), (2,4)(3,5) ] )^G, 0),
         (Group( [ (1,5,6,3), (1,6)(3,5) ] )^G, 0),
         (Group( [ (1,3)(2,4)(5,6), (1,6)(3,5) ] )^G, 0),
         (Group( [ (1,5)(2,4)(3,6), (1,3,2)(4,6,5) ] )^G, 0),
         (Group( [ (1,6)(3,5), (2,4)(3,5), (1,5,6,3) ] )^G, 0),
         (Group( [ (1,6)(3,5), (2,4)(3,5), (1,3,2)(4,6,5) ] )^G, 0),
         (Group( [ (1,6)(3,5), (2,4)(3,5), (1,3,2)(4,6,5), (1,5,6,3) ] )^G, 0)]

    Functorial composition of C_8 and X*C_3::

        sage: G = SymmetricGroup(8).gap()
        sage: G_x = CyclicPermutationGroup(8).gap()
        sage: g_act = libgap.FactorCosetAction(SymmetricGroup(3), [SymmetricGroup(1), CyclicPermutationGroup(3)])
        sage: H = libgap.Group(libgap.MappingGeneratorsImages(g_act)[1])
        sage: l_H = H.ConjugacyClassesSubgroups()
        sage: [(H1, orbits_with_stabilizer(G, G_x, H, H1.Representative())) for H1 in l_H]
        [(Group( () )^G, 816),
         (Group( [ (1,4)(2,3)(5,6)(7,8) ] )^G, 48),
         (Group( [ (1,3,5)(2,4,6) ] )^G, 0),
         (Group( [ (1,3,5)(2,4,6), (1,4)(2,3)(5,6)(7,8) ] )^G, 0)]
    """
    m = points_with_stabilizer(G, G_x, H, H1, verbose=verbose)
    N_H_H1 = libgap.Normalizer(H, H1)
    return m * H1.Size() / N_H_H1.Size()

def points_with_stabilizer(G, G_x, H, H1, verbose=False):
    f = fixed_points(G, G_x, H1, verbose=verbose)
    if H == H1:
        return f
    l_H2 = libgap.IntermediateSubgroups(H, H1)["subgroups"]
    return f - sum(points_with_stabilizer(G, G_x, H, H2, verbose=verbose)
                   for H2 in l_H2)

def fixed_points(G, G_x, H1, verbose=False):
    k = sum(1 for K in G_x.AllSubgroups() if libgap.IsConjugate(G, K, H1))
    N_G_H1 = libgap.Normalizer(G, H1)
    return k * N_G_H1.Size() / G_x.Size()

@DaveWitteMorris
Copy link
Member

I didn't check carefully, but this does seem to be the method that I was suggesting. However, you need libgap.IntermediateSubgroups(H, H1)["subgroups"] to include H, but not include H1, and I don't know whether that's what this method does.

I'm sorry if it can't do any "non-trivial" examples. The method does indeed use AllSubgroups, so it was intended only for cases where H and G_x are small. You actually only need to look at subgroups of H that are conjugate to subgroups of G_x (and subgroups of G_x that are conjugate to subgroups of H), so you might be able to make an improvement there, but I don't think I have time to help you with this.

My only suggestion is that the methods are recursive, so it seems to me that caching (or precomputing the subgroup lattice, orders of normalizers, etc) might speed things up.

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

Successfully merging this pull request may close these issues.

3 participants