diff --git a/src/doc/en/reference/combinat/module_list.rst b/src/doc/en/reference/combinat/module_list.rst index 346065fd219..ffdcd1e7b00 100644 --- a/src/doc/en/reference/combinat/module_list.rst +++ b/src/doc/en/reference/combinat/module_list.rst @@ -28,6 +28,7 @@ Comprehensive Module List sage/combinat/blob_algebra sage/combinat/cartesian_product sage/combinat/catalog_partitions + sage/rings/cfinite_sequence sage/combinat/chas/all sage/combinat/chas/fsym sage/combinat/chas/wqsym @@ -154,6 +155,7 @@ Comprehensive Module List sage/combinat/key_polynomial sage/combinat/knutson_tao_puzzles sage/combinat/k_tableau + sage/rings/lazy_species sage/combinat/lr_tableau sage/combinat/matrices/all sage/combinat/matrices/dancing_links @@ -406,4 +408,3 @@ Comprehensive Module List sage/combinat/words/word_options sage/combinat/words/words sage/combinat/yang_baxter_graph - sage/rings/cfinite_sequence diff --git a/src/doc/en/reference/references/index.rst b/src/doc/en/reference/references/index.rst index 80206ed7f85..c3923b9fe3e 100644 --- a/src/doc/en/reference/references/index.rst +++ b/src/doc/en/reference/references/index.rst @@ -3147,6 +3147,10 @@ REFERENCES: descent algebra.* Adv. Math. **77** (1989). https://web.archive.org/web/20210829143502/http://lacim-membre.uqam.ca/~christo/Publi%C3%A9s/1989/Decomposition%20Solomon.pdf +.. [GL2011] Ira M. Gessel, Ji Li. + *Enumeration of point-determining graphs*. Journal of + Combinatorial Theory, Series A, 118 (2011), pp. 591--612. + .. [GR1993] Ira M. Gessel, Christophe Reutenauer. *Counting Permutations with Given Cycle Structure and Descent Set*. Journal of Combinatorial Theory, Series A, 64 (1993), pp. 189--215. diff --git a/src/sage/combinat/species/all.py b/src/sage/combinat/species/all.py index 1c3a550d342..4dec3677c5f 100644 --- a/src/sage/combinat/species/all.py +++ b/src/sage/combinat/species/all.py @@ -1,9 +1,66 @@ r""" Combinatorial species -.. TODO:: Short blurb about species +A combinatorial species, as introduced by Joyal, is a functor from +the category of finite sets with bijections to the category of finite +sets with bijections. Alternatively, we can regard a combinatorial +species as a formal sum of group actions of the symmetric groups +`\mathfrak S_n`, for `n\in\NN`. For example, the trivial action of +`\mathfrak S_n` corresponds to the species `\mathcal E_n` of sets of +cardinality `n`. -.. TODO:: Proofread / point to the main classes rather than the modules? +More generally, a weighted multisort species in `k` sorts is a +functor from the category of `k`-tuples of finite sets with +bijections to the category of weighted finite sets with +weight-preserving bijections. We may think of the sorts as +variables, traditionally denoted by `X, Y, Z`. + +Such a species is equivalent to a formal sum of group actions of +groups `\mathfrak S_{n_1} \times \cdots \times \mathfrak S_{n_k}` with +`(n_1,\ldots,n_k)\in\NN^k`, together with a weight on the orbits of +each group action. Yet more generally, a virtual weighted multisort +species is a formal difference of weighted multisort species. + +We regard a combinatorial species as a sequence of group actions of +the symmetric groups `\mathfrak S_n`, for `n\in\NN`. + +Coefficients of lazy species are computed on demand. They have +infinite precision, although equality can only be decided in special +cases. + +There are currently two implementations of combinatorial species, one +of which is deprecated and will be removed once all of the +functionality has been ported to the new framework. + +The recommended implementation is :ref:`sage.rings.lazy_species` and +used as the following examples illustrate. + +EXAMPLES: + +We define rooted ordered trees with leaves having weight `q`:: + + sage: R. = QQ[] + sage: L. = LazyCombinatorialSpecies(R) + sage: leaf = X + sage: node = q * X + sage: linorder = X/(1 - X) + sage: T = L.undefined(valuation=1) + sage: T.define(leaf + node * linorder(T)) + sage: T.isotype_generating_series() + X + q*X^2 + ((q^2+q)*X^3) + ((q^3+3*q^2+q)*X^4) + ((q^4+6*q^3+6*q^2+q)*X^5) + + ((q^5+10*q^4+20*q^3+10*q^2+q)*X^6) + O(X^7) + +We define rooted unordered trees with leaves of sort `Y`. The +standard representation of a species is its molecular decomposition:: + + sage: L = LazyCombinatorialSpecies(R, "X") + sage: E = L.Sets() + sage: Ep = E.restrict(1) + sage: M. = LazyCombinatorialSpecies(R) + sage: A = M.undefined(valuation=1) + sage: A.define(Y + X * Ep(A)) + sage: A.truncate(5) + Y + X*Y + (X^2*Y+X*E_2(Y)) + (X^3*Y+X^2*E_2(Y)+X^2*Y^2+X*E_3(Y)) Introductory material --------------------- @@ -39,13 +96,17 @@ - :ref:`sage.combinat.species.structure` - :ref:`sage.combinat.species.misc` + """ # install the docstring of this module to the containing package from sage.misc.namespace_package import install_doc install_doc(__package__, __doc__) from sage.misc.lazy_import import lazy_import -lazy_import("sage.combinat.species.recursive_species", "CombinatorialSpecies") -lazy_import("sage.combinat.species", "library", as_='species') +lazy_import("sage.combinat.species.recursive_species", "CombinatorialSpecies", + deprecation=(38544, "combinat.species is superseded by LazyCombinatorialSpecies")) + +lazy_import("sage.combinat.species", "library", as_='species', + deprecation=(38544, "combinat.species is superseded by LazyCombinatorialSpecies")) del lazy_import del install_doc diff --git a/src/sage/combinat/species/characteristic_species.py b/src/sage/combinat/species/characteristic_species.py index a8078bc7313..cb779788e20 100644 --- a/src/sage/combinat/species/characteristic_species.py +++ b/src/sage/combinat/species/characteristic_species.py @@ -29,6 +29,9 @@ def __repr__(self): EXAMPLES:: sage: F = species.CharacteristicSpecies(3) + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. sage: a = F.structures([1, 2, 3]).random_element(); a {1, 2, 3} sage: F = species.SingletonSpecies() diff --git a/src/sage/combinat/species/composition_species.py b/src/sage/combinat/species/composition_species.py index 17a240b72dc..7c686a52cea 100644 --- a/src/sage/combinat/species/composition_species.py +++ b/src/sage/combinat/species/composition_species.py @@ -27,6 +27,9 @@ def __init__(self, parent, labels, pi, f, gs): TESTS:: sage: E = species.SetSpecies(); C = species.CycleSpecies() + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. sage: L = E(C) sage: a = L.structures(['a','b','c']).random_element() # needs sage.libs.flint sage: a == loads(dumps(a)) # needs sage.libs.flint diff --git a/src/sage/combinat/species/cycle_species.py b/src/sage/combinat/species/cycle_species.py index bc27b988d3c..5b2eeabea77 100644 --- a/src/sage/combinat/species/cycle_species.py +++ b/src/sage/combinat/species/cycle_species.py @@ -25,6 +25,10 @@ def __repr__(self): EXAMPLES:: sage: S = species.CycleSpecies() + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. + sage: S.structures(["a","b","c"])[0] ('a', 'b', 'c') """ diff --git a/src/sage/combinat/species/empty_species.py b/src/sage/combinat/species/empty_species.py index 8ebad9b871d..420860c712a 100644 --- a/src/sage/combinat/species/empty_species.py +++ b/src/sage/combinat/species/empty_species.py @@ -27,6 +27,9 @@ class EmptySpecies(GenericCombinatorialSpecies, UniqueRepresentation): EXAMPLES:: sage: X = species.EmptySpecies(); X + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. Empty species sage: X.structures([]).list() [] diff --git a/src/sage/combinat/species/functorial_composition_species.py b/src/sage/combinat/species/functorial_composition_species.py index 2fc5ede99db..244ac6a2cca 100644 --- a/src/sage/combinat/species/functorial_composition_species.py +++ b/src/sage/combinat/species/functorial_composition_species.py @@ -31,6 +31,9 @@ def __init__(self, F, G, min=None, max=None, weight=None): EXAMPLES:: sage: E = species.SetSpecies() + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. sage: E2 = species.SetSpecies(size=2) sage: WP = species.SubsetSpecies() sage: P2 = E2*E diff --git a/src/sage/combinat/species/generating_series.py b/src/sage/combinat/species/generating_series.py index a325d235cf6..9658631c198 100644 --- a/src/sage/combinat/species/generating_series.py +++ b/src/sage/combinat/species/generating_series.py @@ -216,6 +216,9 @@ def functorial_composition(self, y): EXAMPLES:: sage: G = species.SimpleGraphSpecies() + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. sage: g = G.generating_series() sage: [g.coefficient(i) for i in range(10)] [1, 1, 1, 4/3, 8/3, 128/15, 2048/45, 131072/315, 2097152/315, 536870912/2835] diff --git a/src/sage/combinat/species/library.py b/src/sage/combinat/species/library.py index 4ab233c3bb3..f0b6d8719ee 100644 --- a/src/sage/combinat/species/library.py +++ b/src/sage/combinat/species/library.py @@ -41,6 +41,9 @@ def SimpleGraphSpecies(): EXAMPLES:: sage: S = species.SimpleGraphSpecies() + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. sage: S.generating_series().counts(10) [1, 1, 2, 8, 64, 1024, 32768, 2097152, 268435456, 68719476736] sage: S.cycle_index_series()[:5] # needs sage.modules diff --git a/src/sage/combinat/species/linear_order_species.py b/src/sage/combinat/species/linear_order_species.py index 41a0ac49ae4..b4d4638906b 100644 --- a/src/sage/combinat/species/linear_order_species.py +++ b/src/sage/combinat/species/linear_order_species.py @@ -27,6 +27,9 @@ def canonical_label(self): EXAMPLES:: sage: P = species.LinearOrderSpecies() + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. sage: s = P.structures(["a", "b", "c"]).random_element() sage: s.canonical_label() ['a', 'b', 'c'] diff --git a/src/sage/combinat/species/partition_species.py b/src/sage/combinat/species/partition_species.py index d166a99109a..adf5b5ba5c9 100644 --- a/src/sage/combinat/species/partition_species.py +++ b/src/sage/combinat/species/partition_species.py @@ -33,6 +33,9 @@ def __init__(self, parent, labels, list): sage: from sage.combinat.species.partition_species import PartitionSpeciesStructure sage: P = species.PartitionSpecies() + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. sage: s = PartitionSpeciesStructure(P, ['a','b','c'], [[1,2],[3]]); s {{'a', 'b'}, {'c'}} sage: s == loads(dumps(s)) diff --git a/src/sage/combinat/species/permutation_species.py b/src/sage/combinat/species/permutation_species.py index 7494ee33c08..b100ee1965d 100644 --- a/src/sage/combinat/species/permutation_species.py +++ b/src/sage/combinat/species/permutation_species.py @@ -30,6 +30,10 @@ def canonical_label(self): EXAMPLES:: sage: P = species.PermutationSpecies() + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. + sage: S = P.structures(["a", "b", "c"]) sage: [s.canonical_label() for s in S] [['a', 'b', 'c'], diff --git a/src/sage/combinat/species/product_species.py b/src/sage/combinat/species/product_species.py index 4a17d3c8ed2..ba30ca6d164 100644 --- a/src/sage/combinat/species/product_species.py +++ b/src/sage/combinat/species/product_species.py @@ -27,6 +27,9 @@ def __init__(self, parent, labels, subset, left, right): TESTS:: sage: S = species.SetSpecies() + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. sage: F = S * S sage: a = F.structures(['a','b','c']).random_element() sage: a == loads(dumps(a)) diff --git a/src/sage/combinat/species/recursive_species.py b/src/sage/combinat/species/recursive_species.py index c9bc12a9a4f..5693b08c2d7 100644 --- a/src/sage/combinat/species/recursive_species.py +++ b/src/sage/combinat/species/recursive_species.py @@ -30,6 +30,9 @@ def __init__(self, min=None): EXAMPLES:: sage: F = CombinatorialSpecies() + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. sage: loads(dumps(F)) Combinatorial species diff --git a/src/sage/combinat/species/set_species.py b/src/sage/combinat/species/set_species.py index 3d45fe6b81a..a38a5256776 100644 --- a/src/sage/combinat/species/set_species.py +++ b/src/sage/combinat/species/set_species.py @@ -29,6 +29,9 @@ def __repr__(self): EXAMPLES:: sage: S = species.SetSpecies() + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. sage: a = S.structures(["a","b","c"]).random_element(); a {'a', 'b', 'c'} """ diff --git a/src/sage/combinat/species/species.py b/src/sage/combinat/species/species.py index df68f71fa38..0824968ecd6 100644 --- a/src/sage/combinat/species/species.py +++ b/src/sage/combinat/species/species.py @@ -20,6 +20,9 @@ sage: q = QQ['q'].gen() sage: leaf = species.SingletonSpecies() + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. sage: internal_node = species.SingletonSpecies(weight=q) sage: L = species.LinearOrderSpecies(min=1) sage: T = species.CombinatorialSpecies(min=1) @@ -139,6 +142,9 @@ def __eq__(self, x): sage: X = species.SingletonSpecies() sage: E = species.EmptySetSpecies() sage: L = CombinatorialSpecies() + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. sage: L.define(E+X*L) sage: K = CombinatorialSpecies() sage: K.define(E+X*L) diff --git a/src/sage/combinat/species/structure.py b/src/sage/combinat/species/structure.py index d34005ca138..af112a6aab2 100644 --- a/src/sage/combinat/species/structure.py +++ b/src/sage/combinat/species/structure.py @@ -13,6 +13,9 @@ Here we define this species using the default structures:: sage: ball = species.SingletonSpecies() + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. sage: bar = species.EmptySetSpecies() sage: BB = CombinatorialSpecies() sage: BB.define(ball + ball*BB + ball*bar*BB) diff --git a/src/sage/combinat/species/subset_species.py b/src/sage/combinat/species/subset_species.py index 5515f1c9d96..abbeff18746 100644 --- a/src/sage/combinat/species/subset_species.py +++ b/src/sage/combinat/species/subset_species.py @@ -32,6 +32,9 @@ def __repr__(self): sage: set_random_seed(0) sage: S = species.SubsetSpecies() + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. sage: a = S.structures(["a","b","c"])[0]; a {} """ diff --git a/src/sage/combinat/species/sum_species.py b/src/sage/combinat/species/sum_species.py index c54e7245a55..771ab720bb8 100644 --- a/src/sage/combinat/species/sum_species.py +++ b/src/sage/combinat/species/sum_species.py @@ -32,6 +32,9 @@ def __init__(self, F, G, min=None, max=None, weight=None): EXAMPLES:: sage: S = species.PermutationSpecies() + doctest:warning... + DeprecationWarning: combinat.species is superseded by LazyCombinatorialSpecies + See https://github.com/sagemath/sage/issues/38544 for details. sage: A = S+S sage: A.generating_series()[:5] [2, 2, 2, 2, 2] diff --git a/src/sage/rings/all.py b/src/sage/rings/all.py index ca7946fa78f..3546ec31f61 100644 --- a/src/sage/rings/all.py +++ b/src/sage/rings/all.py @@ -123,6 +123,9 @@ lazy_import('sage.rings.lazy_series_ring', ['LazyLaurentSeriesRing', 'LazyPowerSeriesRing', 'LazySymmetricFunctions', 'LazyDirichletSeriesRing']) +# Lazy combinatorial species +lazy_import('sage.rings.lazy_species', 'LazyCombinatorialSpecies') + # Tate algebras from sage.rings.tate_algebra import TateAlgebra diff --git a/src/sage/rings/lazy_series.py b/src/sage/rings/lazy_series.py index 90da22b8c93..d168404d5f0 100644 --- a/src/sage/rings/lazy_series.py +++ b/src/sage/rings/lazy_series.py @@ -7126,8 +7126,7 @@ def arithmetic_product(self, *args): sage: L = LazySymmetricFunctions(m) # needs sage.modules sage: # needs sage.modules - sage: C = species.CycleSpecies().cycle_index_series() - sage: c = L(lambda n: C[n]) + sage: c = LazyCombinatorialSpecies(QQ, "X").Cycles().cycle_index_series() sage: Lplus = L(lambda n: p([1]*n), valuation=1) sage: r = c.arithmetic_product(Lplus); r # needs sage.libs.pari m[1] + (3*m[1,1]+2*m[2]) diff --git a/src/sage/rings/lazy_species.py b/src/sage/rings/lazy_species.py new file mode 100644 index 00000000000..d8d1da3e09f --- /dev/null +++ b/src/sage/rings/lazy_species.py @@ -0,0 +1,1758 @@ +r""" +Lazy Combinatorial Species + +We regard a combinatorial species as a sequence of group actions of +the symmetric groups `\mathfrak S_n`, for `n\in\NN`. + +Coefficients of lazy species are computed on demand. They have +infinite precision, although equality can only be decided in special +cases. + +AUTHORS: + +- Mainak Roy, Martin Rubey, Travis Scrimshaw (2024-2025) + +EXAMPLES: + +We can reproduce the molecular expansions from Appendix B in +[GL2011]_ with little effort. The molecular expansion of the +species of point determining graphs can be computed as the +species of graphs composed with the compositional inverse of the +species of non-empty sets:: + + sage: L. = LazyCombinatorialSpecies(QQ) + sage: E = L.Sets() + sage: Ep = E.restrict(1) + sage: G = L.Graphs() + +The molecular decomposition begins with:: + + sage: P = G(Ep.revert()) + sage: P.truncate(6) + 1 + X + E_2 + (E_3+X*E_2) + (E_4+X*E_3+E_2(E_2)+X^2*E_2+E_2(X^2)) + + (E_5+E_2*E_3+X*E_4+X*E_2^2+X^2*E_3+2*X*E_2(E_2)+P_5+5*X*E_2(X^2)+3*X^3*E_2) + +Note that [GL2011]_ write `D_5` instead of `P_5`, and there is +apparently a misprint: `X*E_2(E_2) + 4 X^3 E_2` should be `2 X +E_2(E_2) + 3 X^3 E_2`. + +To compute the molecular decomposition of the species of +connected graphs with no endpoints, we use Equation (3.3) in +[GL2011]_. Before that we need to define the species of +connected graphs:: + + sage: Gc = Ep.revert()(G-1) + sage: E_2 = L(SymmetricGroup(2)) + sage: Mc = Gc(X*E(-X)) + E_2(-X) + sage: E(Mc).truncate(5) + 1 + X + E_2 + 2*E_3 + (2*E_4+E_2(E_2)+E_2^2+X*E_3) + +Note that [GL2011]_ apparently contains a misprint: `2 X E_3` +should be `X E_3 + E_2^2`. Indeed, the graphs on four vertices +without endpoints are the complete graph and the empty graph, the +square, the diamond graph and the triangle with an extra isolated +vertex. + +To compute the molecular decomposition of the species of +bi-point-determining graphs we use Corollary (4.6) in +[GL2011]_:: + + sage: B = G(2*Ep.revert() - X) + sage: B.truncate(6) + 1 + X + E_2(X^2) + (P_5+5*X*E_2(X^2)) +""" + +from sage.functions.other import binomial, factorial +from sage.misc.lazy_list import lazy_list +from sage.rings.integer_ring import ZZ +from sage.rings.rational_field import QQ +from sage.rings.lazy_series import (LazyCompletionGradedAlgebraElement, + LazyModuleElement) +from sage.rings.lazy_series_ring import (LazyCompletionGradedAlgebra, + LazyPowerSeriesRing, + LazySymmetricFunctions) +from sage.rings.species import PolynomialSpecies, _label_sets +from sage.data_structures.stream import (Stream_zero, + Stream_exact, + Stream_truncated, + Stream_function) +from sage.categories.tensor import tensor +from sage.combinat.integer_vector import IntegerVectors +from sage.combinat.subset import subsets +from sage.combinat.sf.sf import SymmetricFunctions +from sage.combinat.partition import _Partitions, Partitions +from sage.combinat.permutation import CyclicPermutations +from sage.combinat.set_partition import SetPartitions +from sage.graphs.graph_generators import graphs +from sage.groups.perm_gps.permgroup import PermutationGroup +from sage.groups.perm_gps.permgroup_named import (AlternatingGroup, + CyclicPermutationGroup, + DihedralGroup, + SymmetricGroup) +from sage.misc.inherit_comparison import InheritComparisonClasscallMetaclass +from sage.structure.element import parent +from sage.structure.unique_representation import UniqueRepresentation +import itertools +from collections import defaultdict + + +def weighted_compositions(n, d, weight_multiplicities, _w0=0): + r""" + Return all compositions of `n` of weight `d`. + + The weight of a composition `n_1, n_2, \dots` is `\sum_i w_i n_i`. + + INPUT: + + - ``n`` -- nonnegative integer; the sum of the parts + - ``d`` -- nonnegative integer; the total weight + - ``weight_multiplicities`` -- iterable; + ``weight_multiplicities[i]`` is the number of positions with + weight ``i+1`` + + .. TODO:: + + Possibly this could be merged with + :class:`~sage.combinat.integer_vector_weighted.WeightedIntegerVectors`. + However, that class does not support fixing the sum of the + parts currently. + + EXAMPLES:: + + sage: from sage.rings.lazy_species import weighted_compositions + sage: list(weighted_compositions(1, 1, [2,1])) + [[1, 0], [0, 1]] + + sage: list(weighted_compositions(2, 1, [2,1])) + [] + + sage: list(weighted_compositions(1, 2, [2,1,1])) + [[0, 0, 1]] + + sage: list(weighted_compositions(3, 4, [2,2])) + [[2, 0, 1, 0], + [1, 1, 1, 0], + [0, 2, 1, 0], + [2, 0, 0, 1], + [1, 1, 0, 1], + [0, 2, 0, 1]] + """ + # the empty composition exists if and only if n == d == 0 + if not n: + if not d: + yield [] + return + if not d: + return + + # otherwise we iterate over the possibilities for the first + # weight_multiplicities[_w0] parts + try: + if _w0 >= len(weight_multiplicities): + return + except TypeError: + pass + if _w0 > d: + return + from sage.combinat.integer_lists.invlex import IntegerListsBackend_invlex + for s in range(n + 1): + for c in weighted_compositions(n - s, d - s * (_w0 + 1), weight_multiplicities, _w0=_w0+1): + m = weight_multiplicities[_w0] + for v in IntegerListsBackend_invlex(s, length=m)._iter(): + yield v + c + + +def weighted_vector_compositions(n_vec, d, weight_multiplicities_vec): + r""" + Return all compositions of the vector `n` of weight `d`. + + INPUT: + + - ``n_vec`` -- a `k`-tuple of non-negative integers + + - ``d`` -- a non-negative integer, the total sum of the parts in + all components + + - ``weight_multiplicities_vec`` -- `k`-tuple of iterables, where + ``weight_multiplicities_vec[j][i]`` is the number of + positions with weight `i+1` in the `j`-th component + + EXAMPLES:: + + sage: from sage.rings.lazy_species import weighted_vector_compositions + sage: list(weighted_vector_compositions([1,1], 2, [[2,1,1], [1,1,1]])) + [([1, 0], [1]), ([0, 1], [1])] + + sage: list(weighted_vector_compositions([3,1], 4, [[2,1,0,0,1], [2,1,0,0,1]])) + [([3, 0], [1, 0]), + ([3, 0], [0, 1]), + ([2, 1], [1, 0]), + ([2, 1], [0, 1]), + ([1, 2], [1, 0]), + ([1, 2], [0, 1]), + ([0, 3], [1, 0]), + ([0, 3], [0, 1])] + """ + k = len(n_vec) + from sage.combinat.integer_lists.invlex import IntegerListsBackend_invlex + for d_vec in IntegerListsBackend_invlex(d, length=k)._iter(): + yield from itertools.product(*map(weighted_compositions, + n_vec, d_vec, + weight_multiplicities_vec)) + +###################################################################### + + +class LazyCombinatorialSpeciesElement(LazyCompletionGradedAlgebraElement): + r""" + EXAMPLES: + + Compute the molecular expansion of `E(-X)`:: + + sage: L = LazyCombinatorialSpecies(ZZ, "X") + sage: E = L(SymmetricGroup) + sage: E_inv = 1 / E + sage: E_inv + 1 + (-X) + (-E_2+X^2) + (-E_3+2*X*E_2-X^3) + + (-E_4+2*X*E_3+E_2^2-3*X^2*E_2+X^4) + + (-E_5+2*X*E_4+2*E_2*E_3-3*X^2*E_3-3*X*E_2^2+4*X^3*E_2-X^5) + + (-E_6+2*X*E_5+2*E_2*E_4-3*X^2*E_4+E_3^2-6*X*E_2*E_3+4*X^3*E_3-E_2^3+6*X^2*E_2^2-5*X^4*E_2+X^6) + + O^7 + + Compare with the explicit formula:: + + sage: def coefficient(m): + ....: return sum((-1)^len(la) * multinomial((n := la.to_exp())) * prod(E[i]^ni for i, ni in enumerate(n, 1)) for la in Partitions(m)) + + sage: all(coefficient(m) == E_inv[m] for m in range(10)) + True + """ + def isotype_generating_series(self): + r""" + Return the isotype generating series of ``self``. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: E = L(SymmetricGroup) + sage: E.isotype_generating_series() + 1 + X + X^2 + X^3 + X^4 + X^5 + X^6 + O(X^7) + + sage: C = L(CyclicPermutationGroup, valuation=1) + sage: E(C).isotype_generating_series() + 1 + X + 2*X^2 + 3*X^3 + 5*X^4 + 7*X^5 + 11*X^6 + O(X^7) + + sage: L2. = LazyCombinatorialSpecies(QQ) + sage: E(X + Y).isotype_generating_series() + 1 + (X+Y) + (X^2+X*Y+Y^2) + (X^3+X^2*Y+X*Y^2+Y^3) + + (X^4+X^3*Y+X^2*Y^2+X*Y^3+Y^4) + + (X^5+X^4*Y+X^3*Y^2+X^2*Y^3+X*Y^4+Y^5) + + (X^6+X^5*Y+X^4*Y^2+X^3*Y^3+X^2*Y^4+X*Y^5+Y^6) + + O(X,Y)^7 + + sage: C(X + Y).isotype_generating_series() + (X+Y) + (X^2+X*Y+Y^2) + (X^3+X^2*Y+X*Y^2+Y^3) + + (X^4+X^3*Y+2*X^2*Y^2+X*Y^3+Y^4) + + (X^5+X^4*Y+2*X^3*Y^2+2*X^2*Y^3+X*Y^4+Y^5) + + (X^6+X^5*Y+3*X^4*Y^2+4*X^3*Y^3+3*X^2*Y^4+X*Y^5+Y^6) + + O(X,Y)^7 + """ + P = self.parent() + L = LazyPowerSeriesRing(P.base_ring().fraction_field(), + P._laurent_poly_ring._indices._indices.variable_names()) + if P._arity == 1: + def coefficient(n): + return sum(self[n].coefficients()) + else: + def coefficient(n): + return sum(c * P.base_ring().prod(v ** d for v, d in zip(L.gens(), M.grade())) + for M, c in self[n].monomial_coefficients().items()) + return L(coefficient) + + def generating_series(self): + r""" + Return the (exponential) generating series of ``self``. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: E = L.Sets() + sage: E.generating_series() + 1 + X + 1/2*X^2 + 1/6*X^3 + 1/24*X^4 + 1/120*X^5 + 1/720*X^6 + O(X^7) + + sage: C = L.Cycles() + sage: C.generating_series() + X + 1/2*X^2 + 1/3*X^3 + 1/4*X^4 + 1/5*X^5 + 1/6*X^6 + O(X^7) + + sage: L2. = LazyCombinatorialSpecies(QQ) + sage: E(X + Y).generating_series() + 1 + (X+Y) + (1/2*X^2+X*Y+1/2*Y^2) + + (1/6*X^3+1/2*X^2*Y+1/2*X*Y^2+1/6*Y^3) + + (1/24*X^4+1/6*X^3*Y+1/4*X^2*Y^2+1/6*X*Y^3+1/24*Y^4) + + (1/120*X^5+1/24*X^4*Y+1/12*X^3*Y^2+1/12*X^2*Y^3+1/24*X*Y^4+1/120*Y^5) + + (1/720*X^6+1/120*X^5*Y+1/48*X^4*Y^2+1/36*X^3*Y^3+1/48*X^2*Y^4+1/120*X*Y^5+1/720*Y^6) + + O(X,Y)^7 + + sage: C(X + Y).generating_series() + (X+Y) + (1/2*X^2+X*Y+1/2*Y^2) + (1/3*X^3+X^2*Y+X*Y^2+1/3*Y^3) + + (1/4*X^4+X^3*Y+3/2*X^2*Y^2+X*Y^3+1/4*Y^4) + + (1/5*X^5+X^4*Y+2*X^3*Y^2+2*X^2*Y^3+X*Y^4+1/5*Y^5) + + (1/6*X^6+X^5*Y+5/2*X^4*Y^2+10/3*X^3*Y^3+5/2*X^2*Y^4+X*Y^5+1/6*Y^6) + + O(X,Y)^7 + """ + P = self.parent() + L = LazyPowerSeriesRing(P.base_ring().fraction_field(), + P._laurent_poly_ring._indices._indices.variable_names()) + if P._arity == 1: + def coefficient(n): + return sum(c / M.permutation_group()[0].cardinality() + for M, c in self[n].monomial_coefficients().items()) + else: + def coefficient(n): + return sum(c / M.permutation_group()[0].cardinality() + * P.base_ring().prod(v ** d for v, d in zip(L.gens(), M.grade())) + for M, c in self[n].monomial_coefficients().items()) + return L(coefficient) + + def cycle_index_series(self): + r""" + Return the cycle index series for this species. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(ZZ, "X") + sage: E = L.Sets() + sage: h = SymmetricFunctions(QQ).h() + sage: LazySymmetricFunctions(h)(E.cycle_index_series()) + h[] + h[1] + h[2] + h[3] + h[4] + h[5] + h[6] + O^7 + + sage: s = SymmetricFunctions(QQ).s() + sage: C = L.Cycles() + sage: s(C.cycle_index_series()[5]) + s[1, 1, 1, 1, 1] + s[2, 2, 1] + 2*s[3, 1, 1] + s[3, 2] + s[5] + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: E = L.Sets() + sage: L2. = LazyCombinatorialSpecies(QQ) + sage: E(X + Y).cycle_index_series()[3] + 1/6*p[] # p[1, 1, 1] + 1/2*p[] # p[2, 1] + 1/3*p[] # p[3] + + 1/2*p[1] # p[1, 1] + 1/2*p[1] # p[2] + 1/2*p[1, 1] # p[1] + + 1/6*p[1, 1, 1] # p[] + 1/2*p[2] # p[1] + 1/2*p[2, 1] # p[] + + 1/3*p[3] # p[] + """ + P = self.parent() + p = SymmetricFunctions(P.base_ring().fraction_field()).p() + if P._arity == 1: + L = LazySymmetricFunctions(p) + + def coefficient(n): + return sum(c * M.permutation_group()[0].cycle_index() + for M, c in self[n].monomial_coefficients().items()) + else: + L = LazySymmetricFunctions(tensor([p for _ in range(P._arity)])) + + def coefficient(n): + return sum(c * M.cycle_index() + for M, c in self[n].monomial_coefficients().items()) + + return L(coefficient) + + def restrict(self, min_degree=None, max_degree=None): + r""" + Return the series obtained by keeping only terms of + degree between ``min_degree`` and ``max_degree``. + + INPUT: + + - ``min_degree``, ``max_degree`` -- (optional) integers + indicating which degrees to keep + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(ZZ, "X") + sage: G = L.Graphs() + sage: list(G.isotypes(2)) + [Graph on 2 vertices, Graph on 2 vertices] + + sage: list(G.restrict(2, 2).isotypes(2)) + [Graph on 2 vertices, Graph on 2 vertices] + """ + return RestrictedSpeciesElement(self, min_degree, max_degree) + + def _add_(self, other): + r""" + Return the sum of ``self`` and ``other``. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(ZZ, "X") + sage: E = L(SymmetricGroup) + sage: list(E.structures([1,2,3])) + [(E_3, ((1, 2, 3),))] + sage: list((E+E).structures([1,2,3])) + [((E_3, ((1, 2, 3),)), 'left'), ((E_3, ((1, 2, 3),)), 'right')] + """ + return SumSpeciesElement(self, other) + + def _mul_(self, other): + """ + Return the product of this series with ``other``. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(ZZ, "X") + sage: E = L(SymmetricGroup) + sage: sorted((E^2).structures([1,2,3])) + [((1, ()), (E_3, ((1, 2, 3),))), + ((X, ((1,),)), (E_2, ((2, 3),))), + ((X, ((2,),)), (E_2, ((1, 3),))), + ((X, ((3,),)), (E_2, ((1, 2),))), + ((E_2, ((1, 2),)), (X, ((3,),))), + ((E_2, ((1, 3),)), (X, ((2,),))), + ((E_2, ((2, 3),)), (X, ((1,),))), + ((E_3, ((1, 2, 3),)), (1, ()))] + """ + return ProductSpeciesElement(self, other) + + def structures(self, *labels): + r""" + Iterate over the structures on the given set of labels. + + Generically, this yields a list of pairs consisting of a + molecular species and a relabelled representative of the + cosets of corresponding groups. + + The relabelling is such that the first few labels correspond + to the first factor in the atomic decomposition, etc. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: E = L(SymmetricGroup) + sage: list(E.structures([1,2,3])) + [(E_3, ((1, 2, 3),))] + + sage: C = L(CyclicPermutationGroup, valuation=1) + sage: list(C.structures([1,2,3])) + [(C_3, ((1, 2, 3),)), (C_3, ((1, 3, 2),))] + + sage: F = 1/(2-E) + sage: sorted(F.structures([1,2,3])) + [(E_3, ((1, 2, 3),)), + (X*E_2, ((1,), (2, 3)), 0), + (X*E_2, ((1,), (2, 3)), 1), + (X*E_2, ((2,), (1, 3)), 0), + (X*E_2, ((2,), (1, 3)), 1), + (X*E_2, ((3,), (1, 2)), 0), + (X*E_2, ((3,), (1, 2)), 1), + (X^3, ((1,), (2,), (3,))), + (X^3, ((1,), (3,), (2,))), + (X^3, ((2,), (1,), (3,))), + (X^3, ((2,), (3,), (1,))), + (X^3, ((3,), (1,), (2,))), + (X^3, ((3,), (2,), (1,)))] + + sage: from sage.rings.species import PolynomialSpecies + sage: L = LazyCombinatorialSpecies(QQ, "X, Y") + sage: P = PolynomialSpecies(QQ, "X, Y") + sage: XY = L(P(PermutationGroup([], domain=[1, 2]), {0: [1], 1: [2]})) + sage: list((XY).structures([1], ["a"])) + [(X*Y, ((1,), ('a',)))] + + sage: sorted(E(XY).structures([1,2], [3, 4])) + [((E_2, ((((1, 'X'), (3, 'Y')), ((2, 'X'), (4, 'Y'))),)), + ((X*Y, ((1,), (3,))), (X*Y, ((2,), (4,))))), + ((E_2, ((((1, 'X'), (4, 'Y')), ((2, 'X'), (3, 'Y'))),)), + ((X*Y, ((1,), (4,))), (X*Y, ((2,), (3,)))))] + + sage: list(XY.structures([], [1, 2])) + [] + """ + yield from self[sum(map(len, labels))].structures(*labels) + + def _test_structures(self, tester=None, max_size=4, **options): + r""" + Check that structures and generating series are consistent. + + We check all structures with less than ``max_size`` labels. + + TESTS:: + + sage: from sage.rings.species import PolynomialSpecies + sage: L = LazyCombinatorialSpecies(QQ, "X, Y") + sage: P = PolynomialSpecies(QQ, "X, Y") + sage: XY = L(P(PermutationGroup([], domain=[1, 2, 3]), {0: [1], 1: [2, 3]})) + sage: XY._test_structures() + """ + if tester is None: + tester = self._tester(**options) + P = self.parent() + for n in range(max_size): + if P._arity == 1: + labels = list(range(n)) + s = list(self.structures(labels)) + tester.assertEqual(len(s), len(set(s)), + f"structures for {labels} are {s}, which is not a set") + coeff = self.generating_series()[n] + tester.assertEqual(len(s) / factorial(n), coeff, + f"the number of structures for {labels} is {len(s)}, but the generating series gives {coeff}") + else: + label_shapes = IntegerVectors(n, length=P._arity) + for shape in label_shapes: + labels = [list(range(k)) for k in shape] + s = list(self.structures(*labels)) + tester.assertEqual(len(s), len(set(s)), f"structures for {labels} are {s}, which is not a set") + coeff = self.generating_series()[n].coefficient(list(shape)) + tester.assertEqual(len(s) / ZZ.prod(factorial(k) for k in shape), + coeff, + f"the number of structures for {labels} is {len(s)}, but the generating series gives {coeff}") + + def isotypes(self, *shape): + r""" + Iterate over the isotypes on the given list of sizes. + + Generically, this yields a list of tuples consisting of a + molecular species and, if necessary, an index. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: E = L(SymmetricGroup) + sage: list(E.isotypes(3)) + [(E_3,)] + + sage: P = L(CyclicPermutationGroup, valuation=1) + sage: list(P.isotypes(3)) + [(C_3,)] + + sage: F = 1/(2-E) + sage: sorted(F.isotypes(3)) + [(E_3,), (X*E_2, 0), (X*E_2, 1), (X^3,)] + + sage: from sage.rings.species import PolynomialSpecies + sage: L = LazyCombinatorialSpecies(QQ, "X, Y") + sage: P = PolynomialSpecies(QQ, "X, Y") + sage: XY = L(P(PermutationGroup([], domain=[1, 2]), {0: [1], 1: [2]})) + sage: list((XY).isotypes(1, 1)) + [(X*Y,)] + + sage: list(E(XY).isotypes(2, 2)) + [(E_2(X*Y),)] + """ + multivariate = self.parent()._arity > 1 + shape = tuple(shape) + if not all(e in ZZ for e in shape): + raise NotImplementedError("isotypes with given labels are currently not supported") + for M, c in self[sum(shape)]: + if c not in ZZ or c < 0: + raise NotImplementedError("only implemented for proper non-virtual species") + + if multivariate and tuple(M.grade()) != shape: + continue + + if c == 1: + yield tuple([M]) + else: + for e in range(c): + yield (M, e) + + def _test_isotypes(self, tester=None, max_size=4, **options): + r""" + Check that isotypes and generating series are consistent. + + TESTS:: + + sage: from sage.rings.species import PolynomialSpecies + sage: L = LazyCombinatorialSpecies(QQ, "X, Y") + sage: P = PolynomialSpecies(QQ, "X, Y") + sage: XY = L(P(PermutationGroup([], domain=[1, 2]), {0: [1], 1: [2]})) + sage: XY._test_isotypes(max_size=5) + """ + if tester is None: + tester = self._tester(**options) + P = self.parent() + for n in range(max_size): + if P._arity == 1: + s = list(self.isotypes(n)) + tester.assertEqual(len(s), len(set(s)), + f"isotypes for {n} are {s}, which is not a set") + coeff = self.isotype_generating_series()[n] + tester.assertEqual(len(s), coeff, + f"the number of isotypes for {n} is {len(s)}, but the generating series gives {coeff}") + else: + shapes = IntegerVectors(n, length=P._arity) + for shape in shapes: + s = list(self.isotypes(*shape)) + tester.assertEqual(len(s), len(set(s)), + f"isotypes for {shape} are {s}, which is not a set") + coeff = self.isotype_generating_series()[n].coefficient(list(shape)) + tester.assertEqual(len(s), coeff, + f"the number of isotypes for {shape} is {len(s)}, but the generating series gives {coeff}") + + def polynomial(self, degree=None, names=None): + r""" + Return ``self`` as a polynomial if ``self`` is actually so or up to + specified degree. + + INPUT: + + - ``degree`` -- (optional) integer + - ``names`` -- (default: name of the variables of the series) names of the variables + + OUTPUT: + + If ``degree`` is not ``None``, the terms of the series of + degree greater than ``degree`` are first truncated. If + ``degree`` is ``None`` and the series is not a polynomial + polynomial, a ``ValueError`` is raised. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(ZZ, "X") + sage: E = L(SymmetricGroup) + sage: E.polynomial(3) + 1 + X + E_2 + E_3 + """ + S = self.parent() + R = S._laurent_poly_ring + + if isinstance(self._coeff_stream, Stream_zero): + return R.zero() + + if degree is None: + if (isinstance(self._coeff_stream, Stream_exact) + and not self._coeff_stream._constant): + m = self._coeff_stream._degree + else: + raise ValueError("not a polynomial species") + else: + m = degree + 1 + + return R.sum(self[:m]) + + def __call__(self, *args): + """ + Evaluate ``self`` at ``*args``. + + EXAMPLES:: + + sage: L. = LazyCombinatorialSpecies(QQ) + sage: E2 = L(SymmetricGroup(2)) + sage: E2(E2) + E_2(E_2) + O^11 + + sage: E = L.Sets() + sage: A = L.undefined(1) + sage: A.define(X*E(A)) + sage: A[5] + X*E_4 + X^2*E_3 + 3*X^3*E_2 + X*E_2(X^2) + 3*X^5 + + sage: C = L.Cycles() + sage: F = E(C(A)) + sage: [sum(F[n].monomial_coefficients().values()) for n in range(1, 7)] + [1, 3, 7, 19, 47, 130] + sage: oeis(_) # optional -- internet + 0: A001372: Number of unlabeled mappings (or mapping patterns) + from n points to themselves; number of unlabeled endofunctions. + + sage: R. = QQ[] + sage: L = LazyCombinatorialSpecies(R, "X") + sage: E = L.Sets() + sage: E1 = E.restrict(1) + sage: E(q*E1)[4] + (q^4+q)*E_4 + q^2*E_2(E_2) + q^2*X*E_3 + q^3*E_2^2 + + TESTS:: + + sage: L. = LazyCombinatorialSpecies(QQ) + sage: E2 = L(SymmetricGroup(2)) + sage: X(X + E2) + X + E_2 + O^8 + sage: E2(X + E2) + E_2 + X*E_2 + E_2(E_2) + O^9 + + sage: from sage.rings.species import PolynomialSpecies + sage: P = PolynomialSpecies(QQ, "X") + sage: Gc = L(lambda n: sum(P(G.automorphism_group()) for G in graphs(n) if G.is_connected()) if n else 0) + sage: E = L.Sets() + sage: G = L.Graphs() + sage: E(Gc) - G + O^7 + + sage: (1+E2)(X) + 1 + E_2 + O^7 + + sage: L. = LazyCombinatorialSpecies(QQ) + sage: X(Y, 0) + Y + O^8 + + sage: L1 = LazyCombinatorialSpecies(QQ, "X") + sage: E = L1.Sets() + sage: L. = LazyCombinatorialSpecies(QQ) + sage: E(X) + 1 + X + E_2(X) + E_3(X) + E_4(X) + E_5(X) + E_6(X) + O^7 + + It would be extremely nice to allow the following, but this + poses theoretical problems:: + + sage: L. = LazyCombinatorialSpecies(QQ) + sage: E1 = L.Sets().restrict(1) + sage: Omega = L.undefined(1) + sage: L.define_implicitly([Omega], [E1(Omega) - X]) + sage: Omega[1] # not tested + """ + fP = self.parent() + if len(args) != fP._arity: + raise ValueError("arity of must be equal to the number of arguments provided") + # Find a good parent for the result + from sage.structure.element import get_coercion_model + cm = get_coercion_model() + P = cm.common_parent(self.base_ring(), *[parent(g) for g in args]) + # f = 0 + if isinstance(self._coeff_stream, Stream_zero): + return P.zero() + + # args = (0, ..., 0) + if all((not isinstance(g, LazyModuleElement) and not g) + or (isinstance(g, LazyModuleElement) + and isinstance(g._coeff_stream, Stream_zero)) + for g in args): + return P(self[0]) + + # f is a constant polynomial + if (isinstance(self._coeff_stream, Stream_exact) + and not self._coeff_stream._constant + and self._coeff_stream._degree == 1): + c = self._coeff_stream[0] + B = c.parent() + if B is ZZ or B is QQ or B == self.base_ring(): + return P(c) + return P(c.coefficients()[0]) + + return CompositionSpeciesElement(self, *args) + + def revert(self): + r""" + Return the compositional inverse of ``self``. + + EXAMPLES:: + + sage: L. = LazyCombinatorialSpecies(QQ) + sage: E1 = L.Sets().restrict(1) + sage: g = E1.revert() + sage: g[:5] + [X, -E_2, -E_3 + X*E_2, -E_4 + E_2(E_2) + X*E_3 - X^2*E_2] + + sage: E = L.Sets() + sage: P = E(X*E1(-X))*(1+X) - 1 + sage: P.revert()[:5] + [X, X^2, X*E_2 + 2*X^3, X*E_3 + 2*X^2*E_2 + E_2(X^2) + 5*X^4] + + TESTS:: + + sage: (3 + 2*X).revert() + (-3/2) + 1/2*X + """ + P = self.parent() + if P._arity != 1: + raise ValueError("arity must be equal to 1") + coeff_stream = self._coeff_stream + if isinstance(coeff_stream, Stream_zero): + raise ValueError("compositional inverse does not exist") + R = P._laurent_poly_ring + if (isinstance(coeff_stream, Stream_exact) + and coeff_stream.order() >= 0 + and coeff_stream._degree == 2): + # self = a + b * X; self.revert() = -a/b + 1/b * X + a = coeff_stream[0] + b = coeff_stream[1].coefficients()[0] + X = R(SymmetricGroup(1)) # as a polynomial species + coeff_stream = Stream_exact((-a/b, 1/b * X), + order=0) + return P.element_class(P, coeff_stream) + + # TODO: coefficients should not be checked here, it prevents + # us from using self.define in some cases! + if coeff_stream[0]: + raise ValueError("cannot determine whether the compositional inverse exists") + + X_mol = P._laurent_poly_ring._indices.subset(1)[0] # as a molecular species + X = P(SymmetricGroup(1)) # as a lazy species + + def coefficient(n): + if n: + return 0 + c = coeff_stream[1].coefficient(X_mol) + if c.is_unit(): + return ~c + raise ValueError("compositional inverse does not exist") + + b = P(lambda n: 0 if n else coeff_stream[1].coefficient(X_mol)) # TODO: we want a lazy version of Stream_exact + b_inv = P(coefficient) # TODO: we want a lazy version of Stream_exact + g = P.undefined(valuation=1) + g.define(b_inv * (X - (self - b * X)(g))) + return g + + compositional_inverse = revert + + +class LazyCombinatorialSpeciesElementGeneratingSeriesMixin: + r""" + A lazy species element whose generating series are obtained + by specializing the cycle index series rather than the molecular + expansion. + """ + def isotype_generating_series(self): + r""" + Return the isotype generating series of ``self``. + + The series is obtained by summing the coefficients of the + cycle index series. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: L.Graphs().isotype_generating_series().truncate(8) + 1 + X + 2*X^2 + 4*X^3 + 11*X^4 + 34*X^5 + 156*X^6 + 1044*X^7 + + TESTS:: + + sage: L.Graphs().isotype_generating_series()[20] + 645490122795799841856164638490742749440 + """ + P = self.parent() + L = LazyPowerSeriesRing(P.base_ring().fraction_field(), + P._laurent_poly_ring._indices._indices.variable_names()) + cis = self.cycle_index_series() + return L(lambda n: sum(cis[n].coefficients())) + + def generating_series(self): + r""" + Return the (exponential) generating series of ``self``. + + The series is obtained from the coefficient of `p_{1^n}` of the + cycle index series. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: L.Graphs().generating_series().truncate(7) + 1 + X + X^2 + 4/3*X^3 + 8/3*X^4 + 128/15*X^5 + 2048/45*X^6 + """ + P = self.parent() + L = LazyPowerSeriesRing(P.base_ring().fraction_field(), + P._laurent_poly_ring._indices._indices.variable_names()) + cis = self.cycle_index_series() + return L(lambda n: cis[n].coefficient(_Partitions([1]*n))) + + +class SumSpeciesElement(LazyCombinatorialSpeciesElement): + def __init__(self, left, right): + r""" + Initialize the sum of two species. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: F = L.Sets() + L.SetPartitions() + sage: TestSuite(F).run(skip=['_test_category', '_test_pickling']) + """ + F = super(LazyCombinatorialSpeciesElement, type(left))._add_(left, right) + super().__init__(F.parent(), F._coeff_stream) + self._left = left + self._right = right + + def structures(self, *labels): + r""" + Iterate over the structures on the given set of labels. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: F = L.Sets() + L.SetPartitions() + sage: list(F.structures([1,2,3])) + [((1, 2, 3), 'left'), + ({{1, 2, 3}}, 'right'), + ({{1, 2}, {3}}, 'right'), + ({{1, 3}, {2}}, 'right'), + ({{1}, {2, 3}}, 'right'), + ({{1}, {2}, {3}}, 'right')] + """ + labels = _label_sets(self.parent()._arity, labels) + yield from ((s, 'left') for s in self._left.structures(*labels)) + yield from ((s, 'right') for s in self._right.structures(*labels)) + + +class ProductSpeciesElement(LazyCombinatorialSpeciesElement): + def __init__(self, left, right): + r""" + Initialize the product of two species. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: F = L.Sets() * L.SetPartitions() + sage: TestSuite(F).run(skip=['_test_category', '_test_pickling']) + """ + F = super(LazyCombinatorialSpeciesElement, type(left))._mul_(left, right) + super().__init__(F.parent(), F._coeff_stream) + self._left = left + self._right = right + + def structures(self, *labels): + r""" + Iterate over the structures on the given set of labels. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(ZZ, "X") + sage: E = L.Sets() + sage: C = L.Cycles() + sage: P = E * C + sage: list(P.structures([1,2])) + [((), (1, 2)), ((1,), (2,)), ((2,), (1,))] + + sage: P = E * E + sage: list(P.structures([1,2])) + [((), (1, 2)), ((1,), (2,)), ((2,), (1,)), ((1, 2), ())] + + sage: L. = LazyCombinatorialSpecies(QQ) + sage: list((X*Y).structures([1], [2])) + [((X, ((1,),)), (Y, ((2,),)))] + """ + def dissections(s): + for subset in subsets(s): + subset_set = set(subset) + yield (subset, tuple([e for e in s if e not in subset_set])) + + labels = _label_sets(self.parent()._arity, labels) + for d in itertools.product(*[dissections(u) for u in labels]): + yield from itertools.product(self._left.structures(*[U for U, _ in d]), + self._right.structures(*[V for _, V in d])) + + +class CompositionSpeciesElement(LazyCombinatorialSpeciesElement): + def __init__(self, left, *args): + r""" + Initialize the composition of species. + + TESTS:: + + sage: L. = LazyCombinatorialSpecies(QQ) + sage: L.zero()(X) + 0 + sage: X(L.zero()) + 0 + sage: (1+X)(L.zero()) + 1 + + sage: L2. = LazyCombinatorialSpecies(QQ) + sage: F = L.Sets()(X + 2*Y) + sage: TestSuite(F).run(skip=['_test_category', '_test_pickling']) + """ + fP = left.parent() + # Find a good parent for the result + from sage.structure.element import get_coercion_model + cm = get_coercion_model() + P = cm.common_parent(left.base_ring(), *[parent(g) for g in args]) + + args = [P(g) for g in args] + + for g in args: + if g._coeff_stream._approximate_order == 0: + if not g._coeff_stream.is_uninitialized() and g[0]: + raise ValueError("can only compose with a positive valuation series") + g._coeff_stream._approximate_order = 1 + + sorder = left._coeff_stream._approximate_order + gv = min(g._coeff_stream._approximate_order for g in args) + R = P._internal_poly_ring.base_ring() + L = fP._internal_poly_ring.base_ring() + + def coeff(g, i): + c = g._coeff_stream[i] + if not isinstance(c, PolynomialSpecies.Element): + return R(c) + return c + + # args_flat and weights contain one list for each g + weight_exp = [lazy_list(lambda j, g=g: len(coeff(g, j+1))) + for g in args] + + def flat(g): + # function needed to work around python's scoping rules + return itertools.chain.from_iterable((coeff(g, j) for j in itertools.count())) + + args_flat1 = [lazy_list(flat(g)) for g in args] + + def coefficient(n): + if not n: + if left[0]: + return R(list(left[0])[0][1]) + return R.zero() + result = R.zero() + for i in range(1, n // gv + 1): + # skip i=0 because it produces a term only for n=0 + + # compute homogeneous components + lF = defaultdict(L) + for M, c in left[i]: + lF[M.grade()] += L._from_dict({M: c}) + for mc, F in lF.items(): + for degrees in weighted_vector_compositions(mc, n, weight_exp): + args_flat = [list(a[0:len(degrees[j])]) + for j, a in enumerate(args_flat1)] + multiplicities = [c for alpha, g_flat in zip(degrees, args_flat) + for d, (_, c) in zip(alpha, g_flat) if d] + molecules = [M for alpha, g_flat in zip(degrees, args_flat) + for d, (M, _) in zip(alpha, g_flat) if d] + non_zero_degrees = [[d for d in alpha if d] for alpha in degrees] + names = ["X%s" % i for i in range(len(molecules))] + FX = F._compose_with_weighted_singletons(names, + multiplicities, + non_zero_degrees) + FG = [(M(*molecules), c) for M, c in FX] + result += R.sum_of_terms(FG) + return result + + coeff_stream = Stream_function(coefficient, P._sparse, sorder * gv) + super().__init__(P, coeff_stream) + self._left = left + self._args = args + + def structures(self, *labels): + r""" + Iterate over the structures on the given set of labels. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: E = L.Sets() + sage: E1 = L.Sets().restrict(1) + sage: sorted(E(E1).structures([1,2,3])) + [((((1, 'X'),), ((2, 'X'),), ((3, 'X'),)), ((1,), (2,), (3,))), + ((((1, 'X'),), ((2, 'X'), (3, 'X'))), ((1,), (2, 3))), + ((((1, 'X'), (2, 'X')), ((3, 'X'),)), ((1, 2), (3,))), + ((((1, 'X'), (2, 'X'), (3, 'X')),), ((1, 2, 3),)), + ((((1, 'X'), (3, 'X')), ((2, 'X'),)), ((1, 3), (2,)))] + + sage: C = L.Cycles() + sage: L. = LazyCombinatorialSpecies(QQ) + sage: sum(1 for s in C(X*Y).structures([1,2,3], [1,2,3])) + 12 + + sage: C(X*Y).generating_series()[6] + 1/3*X^3*Y^3 + + sage: sum(1 for s in E(X*Y).structures([1,2,3], ["a", "b", "c"])) + 6 + """ + F = self._left + G = self._args + m = len(G) # == F.parent()._arity + k = self.parent()._arity # == G[i].parent()._arity + names = self.parent()._laurent_poly_ring._indices._indices._names + labels = _label_sets(k, labels) + # make label sets disjoint + U = [(e, i) for l, i in zip(labels, names) for e in l] + + def split_set(C): + C_split = defaultdict(list) + for e, i in C: + C_split[i].append(e) + return [C_split[i] for i in names] + + Par_U = SetPartitions(U) + for pi in Par_U: + # Fix an arbitrary order of the blocks + try: + pi_list = sorted([sorted(b) for b in pi]) + except TypeError: + pi_list = sorted([sorted(b, key=str) for b in pi], key=str) + + # Generate all functions chi from pi to {0, ..., m-1} + for chi in itertools.product(range(m), repeat=len(pi_list)): + chi_inv = defaultdict(list) + for b, i in zip(pi_list, chi): + chi_inv[i].append(b) + + # The set of structures is the Cartesian product of + # the structures in F[chi_inv[i] for i in range(m)] + # and for each set C in chi_inv[i] the set of + # structures in G_i[C] + F_s = F.structures(*[[tuple(b) for b in chi_inv[i]] for i in range(m)]) + G_s = [G[i].structures(*split_set(C)) for i in range(m) for C in chi_inv[i]] + yield from itertools.product(F_s, itertools.product(*G_s)) + + +class LazyCombinatorialSpecies(LazyCompletionGradedAlgebra): + Element = LazyCombinatorialSpeciesElement + + @staticmethod + def __classcall_private__(cls, base_ring, names, sparse=True): + """ + Normalize input to ensure a unique representation. + + TESTS:: + + sage: LazyCombinatorialSpecies(QQ, "X") is LazyCombinatorialSpecies(QQ, "X") + True + """ + from sage.structure.category_object import normalize_names + names = normalize_names(-1, names) + if len(names) == 1: + return LazyCombinatorialSpeciesUnivariate(base_ring, names, sparse) + return LazyCombinatorialSpeciesMultivariate(base_ring, names, sparse) + + def _first_ngens(self, n): + r""" + Used by the preparser for ``F. = ...``. + + We do not use the generic implementation of + :class:`sage.combinat.CombinatorialFreeModule`, because we do + not want to implement `gens`. + + EXAMPLES:: + + sage: L. = LazyCombinatorialSpecies(QQ) # indirect doctest + sage: 1/(1-X-Y) + 1 + (X+Y) + (X^2+2*X*Y+Y^2) + (X^3+3*X^2*Y+3*X*Y^2+Y^3) + + (X^4+4*X^3*Y+6*X^2*Y^2+4*X*Y^3+Y^4) + + (X^5+5*X^4*Y+10*X^3*Y^2+10*X^2*Y^3+5*X*Y^4+Y^5) + + (X^6+6*X^5*Y+15*X^4*Y^2+20*X^3*Y^3+15*X^2*Y^4+6*X*Y^5+Y^6) + O^7 + """ + return tuple([self(g) for g in self._laurent_poly_ring._first_ngens(n)]) + + def __init__(self, base_ring, names, sparse): + r""" + The ring of lazy species. + + EXAMPLES: + + We provide univariate and multivariate (mostly known as + multisort) species:: + + sage: LazyCombinatorialSpecies(QQ, "X") + Lazy completion of Polynomial species in X over Rational Field + + sage: LazyCombinatorialSpecies(QQ, "X, Y") + Lazy completion of Polynomial species in X, Y over Rational Field + + In the univariate case, several basic species are provided as + methods:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: L.Sets() + Set species + sage: L.Cycles() + Cycle species + sage: L.OrientedSets() + Oriented Set species + sage: L.Polygons() + Polygon species + sage: L.Graphs() + Graph species + sage: L.SetPartitions() + Set Partition species + + TESTS:: + + sage: LazyCombinatorialSpecies(QQ, "X, Y, Z")._arity + 3 + """ + super().__init__(PolynomialSpecies(base_ring, names)) + self._arity = len(names) + + +class LazyCombinatorialSpeciesUnivariate(LazyCombinatorialSpecies): + def Sets(self): + r""" + Return the species of sets. + + This species corresponds to the sequence of trivial group + actions. Put differently, the stabilizers are the full + symmetric groups. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: G = L.Sets() + sage: set(G.isotypes(4)) + {(E_4,)} + sage: set(G.structures(["a", 1, x])) + {(1, 'a', x)} + """ + return SetSpecies(self) + + def Cycles(self): + r""" + Return the species of (oriented) cycles. + + This species corresponds to the sequence of group actions + having the cyclic groups as stabilizers. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: G = L.Cycles() + sage: set(G.isotypes(4)) + {(C_4,)} + sage: set(G.structures(["a", "b", "c"])) + {('a', 'b', 'c'), ('a', 'c', 'b')} + """ + return CycleSpecies(self) + + def Polygons(self): + r""" + Return the species of polygons. + + Polygons are cycles up to orientation. + + This species corresponds to the sequence of group actions + having the dihedral groups as stabilizers. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: G = L.Polygons() + sage: set(G.isotypes(5)) + {(P_5,)} + sage: set(G.structures(["a", 1, "b", 2])) + {(E_2(E_2), ((1, 'a', 2, 'b'),)), + (E_2(E_2), ((1, 'b', 2, 'a'),)), + (E_2(E_2), ((1, 2, 'a', 'b'),))} + """ + return PolygonSpecies(self) + + def OrientedSets(self): + r""" + Return the species of oriented sets. + + Oriented sets are total orders up to an even orientation. + + This species corresponds to the sequence of group actions + having the alternating groups as stabilizers. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: G = L.OrientedSets() + sage: set(G.isotypes(5)) + {(Eo_5,)} + sage: set(G.structures(["a", 1, "b", 2])) + {(Eo_4, ((1, 2, 'a', 'b'),)), (Eo_4, ((1, 2, 'b', 'a'),))} + """ + return OrientedSetSpecies(self) + + def Chains(self): + r""" + Return the species of chains. + + Chains are linear orders up to reversal. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: Ch = L.Chains() + sage: set(Ch.isotypes(4)) + {(E_2(X^2),)} + sage: list(Ch.structures(["a", "b", "c"])) + [('a', 'c', 'b'), ('a', 'b', 'c'), ('b', 'a', 'c')] + """ + return ChainSpecies(self) + + def Graphs(self): + r""" + Return the species of vertex labelled simple graphs. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: G = L.Graphs() + sage: set(G.isotypes(2)) + {Graph on 2 vertices, Graph on 2 vertices} + + sage: G.isotype_generating_series()[20] + 645490122795799841856164638490742749440 + """ + return GraphSpecies(self) + + def SetPartitions(self): + r""" + Return the species of set partitions. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: G = L.SetPartitions() + sage: set(G.isotypes(4)) + {[1, 1, 1, 1], [2, 1, 1], [2, 2], [3, 1], [4]} + sage: list(G.structures(["a", "b", "c"])) + [{{'a', 'b', 'c'}}, + {{'a', 'b'}, {'c'}}, + {{'a', 'c'}, {'b'}}, + {{'a'}, {'b', 'c'}}, + {{'a'}, {'b'}, {'c'}}] + """ + return SetPartitionSpecies(self) + + +class LazyCombinatorialSpeciesMultivariate(LazyCombinatorialSpecies): + pass + + +class SetSpecies(LazyCombinatorialSpeciesElement, UniqueRepresentation, + metaclass=InheritComparisonClasscallMetaclass): + def __init__(self, parent): + r""" + Initialize the species of sets. + + TESTS:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: E = L.Sets() + sage: TestSuite(E).run(skip=['_test_category', '_test_pickling']) + + sage: E is L.Sets() + True + """ + S = parent(SymmetricGroup) + super().__init__(parent, S._coeff_stream) + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: LazyCombinatorialSpecies(QQ, "X").Sets() # indirect doctest + Set species + """ + return "Set species" + + def structures(self, labels): + r""" + Iterate over the structures on the given set of labels. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(ZZ, "X") + sage: E = L.Sets() + sage: list(E.structures([1,2,3])) + [(1, 2, 3)] + """ + labels = _label_sets(self.parent()._arity, [labels]) + yield labels[0] + + +class CycleSpecies(LazyCombinatorialSpeciesElement, UniqueRepresentation, + metaclass=InheritComparisonClasscallMetaclass): + def __init__(self, parent): + r""" + Initialize the species of cycles. + + TESTS:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: C = L.Cycles() + sage: TestSuite(C).run(skip=['_test_category', '_test_pickling']) + + sage: C is L.Cycles() + True + """ + S = parent(CyclicPermutationGroup, valuation=1) + super().__init__(parent, S._coeff_stream) + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: LazyCombinatorialSpecies(QQ, "X").Cycles() # indirect doctest + Cycle species + """ + return "Cycle species" + + def structures(self, labels): + r""" + Iterate over the structures on the given set of labels. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(ZZ, "X") + sage: C = L.Cycles() + sage: list(C.structures([])) + [] + sage: list(C.structures([1])) + [(1,)] + sage: list(C.structures([1,2])) + [(1, 2)] + sage: list(C.structures([1,2,3])) + [(1, 2, 3), (1, 3, 2)] + """ + labels = _label_sets(self.parent()._arity, [labels]) + # TODO: CyclicPermutations should yield hashable objects, not lists + yield from map(tuple, CyclicPermutations(labels[0])) + + +class PolygonSpecies(LazyCombinatorialSpeciesElement, UniqueRepresentation, + metaclass=InheritComparisonClasscallMetaclass): + def __init__(self, parent): + r""" + Initialize the species of polygons. + + TESTS:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: P = L.Polygons() + sage: TestSuite(P).run(skip=['_test_category', '_test_pickling']) + + sage: P is L.Polygons() + True + """ + S = parent(DihedralGroup, valuation=3) + super().__init__(parent, S._coeff_stream) + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: LazyCombinatorialSpecies(QQ, "X").Polygons() # indirect doctest + Polygon species + """ + return "Polygon species" + + +class OrientedSetSpecies(LazyCombinatorialSpeciesElement, UniqueRepresentation, + metaclass=InheritComparisonClasscallMetaclass): + def __init__(self, parent): + r""" + Initialize the species of polygons. + + TESTS:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: Eo = L.OrientedSets() + sage: TestSuite(Eo).run(skip=['_test_category', '_test_pickling']) + + sage: Eo is L.OrientedSets() + True + """ + S = parent(AlternatingGroup, valuation=4) + super().__init__(parent, S._coeff_stream) + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: LazyCombinatorialSpecies(QQ, "X").OrientedSets() # indirect doctest + Oriented Set species + """ + return "Oriented Set species" + + +class ChainSpecies(LazyCombinatorialSpeciesElement, UniqueRepresentation, + metaclass=InheritComparisonClasscallMetaclass): + def __init__(self, parent): + r""" + Initialize the species of chains. + + TESTS:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: Ch = L.Chains() + sage: TestSuite(Ch).run(skip=['_test_category', '_test_pickling']) + + sage: Ch is L.Chains() + True + """ + P = parent._laurent_poly_ring + + def coefficient(n): + if not n: + return P.one() + if n % 2: + gen = [(i, i+1) for i in range(2, n+1, 2)] + else: + gen = [(i, i+1) for i in range(1, n+1, 2)] + return P(PermutationGroup([gen])) + + S = parent(coefficient) + super().__init__(parent, S._coeff_stream) + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: LazyCombinatorialSpecies(QQ, "X").Chains() # indirect doctest + Chain species + """ + return "Chain species" + + def structures(self, labels): + r""" + Iterate over the structures on the given set of labels. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(ZZ, "X") + sage: Ch = L.Chains() + sage: list(Ch.structures([1,2,3])) + [(1, 3, 2), (1, 2, 3), (2, 1, 3)] + """ + labels = _label_sets(self.parent()._arity, [labels])[0] + n = len(labels) + if not n: + yield () + elif n == 1: + yield labels + else: + for a, b in itertools.combinations(labels, 2): + ia = labels.index(a) + ib = labels.index(b) + rest = labels[:ia] + labels[ia+1:ib] + labels[ib+1:] + for pi in itertools.permutations(rest): + yield (a,) + pi + (b,) + + +class GraphSpecies(LazyCombinatorialSpeciesElementGeneratingSeriesMixin, + LazyCombinatorialSpeciesElement, UniqueRepresentation, + metaclass=InheritComparisonClasscallMetaclass): + def __init__(self, parent): + r""" + Initialize the species of simple graphs. + + TESTS:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: G = L.Graphs() + sage: TestSuite(G).run(skip=['_test_category', '_test_pickling']) + + sage: G is L.Graphs() + True + """ + P = parent._laurent_poly_ring + S = parent(lambda n: sum(P(G.automorphism_group()) for G in graphs(n))) + super().__init__(parent, S._coeff_stream) + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: LazyCombinatorialSpecies(QQ, "X").Graphs() # indirect doctest + Graph species + """ + return "Graph species" + + def isotypes(self, labels): + r""" + Iterate over the isotypes on the given list of sizes. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: G = L.Graphs() + sage: list(G.isotypes(2)) + [Graph on 2 vertices, Graph on 2 vertices] + """ + if labels in ZZ: + yield from (G.canonical_label().copy(immutable=True) for G in graphs(labels)) + else: + raise NotImplementedError("isotypes with given labels are currently not supported") + + def generating_series(self): + r""" + Return the (exponential) generating series of the + species of simple graphs. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: L.Graphs().generating_series().truncate(7) + 1 + X + X^2 + 4/3*X^3 + 8/3*X^4 + 128/15*X^5 + 2048/45*X^6 + """ + P = self.parent() + L = LazyPowerSeriesRing(P.base_ring().fraction_field(), + P._laurent_poly_ring._indices._indices.variable_names()) + return L(lambda n: 2**binomial(n, 2) / factorial(n)) + + def cycle_index_series(self): + r""" + Return the cycle index series of the species of simple graphs. + + The cycle index series is computed using Proposition 2.2.7 in + [BLL1998]_. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: L.Graphs().cycle_index_series().truncate(4) + p[] + p[1] + (p[1,1]+p[2]) + (4/3*p[1,1,1]+2*p[2,1]+2/3*p[3]) + + Check that the number of isomorphism types is computed quickly:: + + sage: L.Graphs().isotype_generating_series()[20] + 645490122795799841856164638490742749440 + """ + P = self.parent() + p = SymmetricFunctions(P.base_ring().fraction_field()).p() + L = LazySymmetricFunctions(p) + + def a(sigma): + rho = sigma.to_exp() + res1 = ZZ.sum(ZZ(i+1)._gcd(ZZ(j+1)) * rho[i] * rho[j] + for i in range(len(rho)) + for j in range(i+1, len(rho))) + res2 = ZZ.sum(ZZ(i+1) * rho[i]**2 + for i in range(len(rho))) + res3 = ZZ.sum(rho[::2]) + return ZZ(2) ** (res1 + (res2 - res3) / 2) / sigma.centralizer_size() + + def coefficient(n): + return p._from_dict({sigma: a(sigma) for sigma in Partitions(n)}) + + return L(coefficient) + + +class SetPartitionSpecies(LazyCombinatorialSpeciesElement, UniqueRepresentation, + metaclass=InheritComparisonClasscallMetaclass): + def __init__(self, parent): + r""" + Initialize the species of set partitions. + + TESTS:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: p = L.SetPartitions() + sage: TestSuite(p).run(skip=['_test_category', '_test_pickling']) + + sage: p is L.SetPartitions() + True + """ + E = parent.Sets() + E1 = parent.Sets().restrict(1) + super().__init__(parent, E(E1)._coeff_stream) + + def _repr_(self): + r""" + Return a string representation of ``self``. + + EXAMPLES:: + + sage: LazyCombinatorialSpecies(QQ, "X").SetPartitions() # indirect doctest + Set Partition species + """ + return "Set Partition species" + + def isotypes(self, labels): + r""" + Iterate over the isotypes on the given list of sizes. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: p = L.SetPartitions() + sage: list(p.isotypes(3)) + [[3], [2, 1], [1, 1, 1]] + """ + if labels in ZZ: + yield from Partitions(labels) + else: + raise NotImplementedError("isotypes with given labels are currently not supported") + + def structures(self, labels): + r""" + Iterate over the structures on the given set of labels. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(ZZ, "X") + sage: P = L.SetPartitions() + sage: list(P.structures([])) + [{}] + sage: list(P.structures([1])) + [{{1}}] + sage: list(P.structures([1,2])) + [{{1, 2}}, {{1}, {2}}] + sage: list(P.structures([1,2,3])) + [{{1, 2, 3}}, {{1, 2}, {3}}, {{1, 3}, {2}}, {{1}, {2, 3}}, {{1}, {2}, {3}}] + """ + labels = _label_sets(self.parent()._arity, [labels]) + yield from SetPartitions(labels[0]) + + +class RestrictedSpeciesElement(LazyCombinatorialSpeciesElement): + def __init__(self, F, min_degree, max_degree): + r""" + Initialize the restriction of a species to the given degrees. + + TESTS:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: G3 = L.Graphs().restrict(3, 3) + sage: TestSuite(G3).run(skip=['_test_category', '_test_pickling']) + """ + self._F = F + self._min = min_degree + self._max = max_degree + + if max_degree is None and min_degree is None: + coeff_stream = F._coeff_stream + elif max_degree is None: + v = max(F._coeff_stream._approximate_order, min_degree) + coeff_stream = Stream_truncated(F._coeff_stream, 0, v) + else: + if min_degree is None: + v = F._coeff_stream._approximate_order + else: + v = max(F._coeff_stream._approximate_order, min_degree) + initial_coefficients = [F._coeff_stream[i] for i in range(v, max_degree + 1)] + if not any(initial_coefficients): + coeff_stream = Stream_zero() + else: + coeff_stream = Stream_exact(initial_coefficients, order=v) + + super().__init__(F.parent(), coeff_stream) + + def isotypes(self, *shape): + r""" + Iterate over the isotypes on the given list of sizes. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: p = L.SetPartitions().restrict(2, 2) + sage: list(p.isotypes(3)) + [] + """ + n = sum(shape) + if ((self._min is None or self._min <= n) + and (self._max is None or n <= self._max)): + yield from self._F.isotypes(*shape) + + def structures(self, *labels): + r""" + Iterate over the structures on the given set of labels. + + EXAMPLES:: + + sage: L = LazyCombinatorialSpecies(ZZ, "X") + sage: F = L.SetPartitions().restrict(3) + sage: list(F.structures([1])) + [] + sage: list(F.structures([1,2,3])) + [{{1, 2, 3}}, {{1, 2}, {3}}, {{1, 3}, {2}}, {{1}, {2, 3}}, {{1}, {2}, {3}}] + """ + n = sum(map(len, labels)) + if ((self._min is None or self._min <= n) + and (self._max is None or n <= self._max)): + yield from self._F.structures(*labels) diff --git a/src/sage/rings/meson.build b/src/sage/rings/meson.build index 573342a9508..55a99548aca 100644 --- a/src/sage/rings/meson.build +++ b/src/sage/rings/meson.build @@ -38,6 +38,7 @@ py.install_sources( 'laurent_series_ring_element.pxd', 'lazy_series.py', 'lazy_series_ring.py', + 'lazy_species.py', 'localization.py', 'monomials.py', 'morphism.pxd', diff --git a/src/sage/rings/species.py b/src/sage/rings/species.py index 018375ba74e..e30f3247ecd 100644 --- a/src/sage/rings/species.py +++ b/src/sage/rings/species.py @@ -45,6 +45,7 @@ from sage.combinat.integer_vector import IntegerVectors from sage.combinat.integer_vector_weighted import WeightedIntegerVectors from sage.combinat.partition import Partitions, _Partitions +from sage.combinat.set_partition_ordered import OrderedSetPartitions from sage.combinat.sf.sf import SymmetricFunctions from sage.groups.perm_gps.constructor import PermutationGroupElement from sage.groups.perm_gps.permgroup import PermutationGroup, PermutationGroup_generic @@ -72,6 +73,47 @@ GAP_FAIL = libgap.eval('fail') +def _label_sets(arity, labels): + r""" + Return labels as a list of tuples. + + INPUT: + + - ``arity`` -- the arity of the species + - ``labels`` -- an iterable of iterables + + EXAMPLES:: + + sage: from sage.rings.species import _label_sets + sage: _label_sets(2, [[1, 2], ["x", "y", "z"]]) + [(1, 2), ('x', 'y', 'z')] + + TESTS:: + + sage: _label_sets(2, [[1, 1], ["x", "y", "z"]]) + Traceback (most recent call last): + ... + ValueError: The argument labels must be a set, but [[1, 1], ['x', 'y', 'z']] has duplicates + + sage: _label_sets(2, [[1, 2]]) + Traceback (most recent call last): + ... + ValueError: arity of must be equal to the number of arguments provided + """ + if len(labels) != arity: + raise ValueError("arity of must be equal to the number of arguments provided") + + label_sets = [set(U) for U in labels] + if not all(len(U) == len(V) for U, V in zip(labels, label_sets)): + raise ValueError(f"The argument labels must be a set, but {labels} has duplicates") + try: + label_sets_sorted = [sorted(x) for x in label_sets] + except TypeError: + label_sets_sorted = [sorted(x, key=str) for x in label_sets] + + return [tuple(U) for U in label_sets_sorted] + + class AtomicSpeciesElement(WithEqualityById, Element, WithPicklingByInitArgs, @@ -348,6 +390,49 @@ def __le__(self, other): """ return self is other or self < other + def structures(self, *labels): + r""" + Iterate over the structures on the given set of labels. + + This yields a list of relabelled representatives of the + cosets of corresponding groups. + + The relabelling is such that the first few labels correspond + to the first sort, etc. + + EXAMPLES:: + + sage: from sage.rings.species import AtomicSpecies + sage: A = AtomicSpecies("X, Y") + sage: G = PermutationGroup([[("a", "b", "c", "d"), ("e", "f")]]) + sage: a = A(G, {0: "abcd", 1: "ef"}) + sage: list(a.structures([1, 2, 3, 4], ["a", "b"])) + [(1, 2, 3, 4, 'a', 'b'), + (1, 2, 3, 4, 'b', 'a'), + (1, 2, 4, 3, 'a', 'b'), + (1, 2, 4, 3, 'b', 'a'), + (1, 3, 2, 4, 'a', 'b'), + (1, 3, 2, 4, 'b', 'a'), + (1, 3, 4, 2, 'a', 'b'), + (1, 3, 4, 2, 'b', 'a'), + (1, 4, 2, 3, 'a', 'b'), + (1, 4, 2, 3, 'b', 'a'), + (1, 4, 3, 2, 'a', 'b'), + (1, 4, 3, 2, 'b', 'a')] + + sage: G = PermutationGroup([[(2,3),(4,5)]], domain=[2,3,4,5]) + sage: a = A(G, {0: [2, 3], 1: [4, 5]}) + sage: list(a.structures([1, 2],["a", "b"])) + [(1, 2, 'a', 'b'), (1, 2, 'b', 'a')] + """ + labels = _label_sets(self.parent()._arity, labels) + n = tuple([len(U) for U in labels]) + S = SymmetricGroup(sum(n)).young_subgroup(n) + l = [e for l in labels for e in l] + if self._mc == n: + for rep in libgap.RightTransversal(S, self._dis): + yield tuple(S(rep)._act_on_list_on_position(l)) + class AtomicSpecies(UniqueRepresentation, Parent): r""" @@ -785,7 +870,13 @@ def _stabilizer_subgroups(G, X, a, side='right', check=True): sage: X = [1, 2, 3] sage: _stabilizer_subgroups(G, X, lambda pi, x: pi(x), side='left') [Permutation Group with generators [(2,3)]] + + Be warned that the product in permutation groups is left-to-right composition:: + sage: _stabilizer_subgroups(G, X, lambda x, pi: pi(x), side='right') + [Permutation Group with generators [(2,3)]] + + sage: _stabilizer_subgroups(G, X, lambda x, pi: pi.inverse()(x), side='right') Traceback (most recent call last): ... ValueError: The given function is not a right group action: g=(1,3,2), h=(2,3), x=1 do not satisfy the condition @@ -809,7 +900,7 @@ def _stabilizer_subgroups(G, X, a, side='right', check=True): elif side == "right": if check: for g, h, x in product(G, G, X): - if not a(x, h * g) == a(a(x, g), h): + if not a(x, h * g) == a(a(x, h), g): raise ValueError(f"The given function is not a right group action: g={g}, h={h}, x={x} do not satisfy the condition") g_orbits = [orbit_decomposition(X_set, lambda x: a(x, g)) @@ -1375,11 +1466,27 @@ def cycle_index(self, parent=None): sage: A.cycle_index() 1/4*p[1, 1] # p[1, 1, 1, 1] + 1/4*p[1, 1] # p[2, 2] + 1/4*p[2] # p[1, 1, 1, 1] + 1/4*p[2] # p[2, 2] + Find two molecular species with the same cycle index:: + + sage: M = MolecularSpecies("X") + sage: n = 6 + sage: Ms = M.subset(n) # long time + sage: Cs = [m.cycle_index() for m in Ms] # long time + sage: d = [m for m in Ms if Cs.count(m.cycle_index()) > 1] # long time + sage: len(d) # long time + 2 + sage: Pb_4 = M(PermutationGroup([[(1,2), (3,4)], [(1,4), (2,3)]])) + sage: X = M(SymmetricGroup(1)) + sage: X^2*Pb_4 in d # long time + True + TESTS: Check that we support different parents:: sage: F = CombinatorialFreeModule(QQ, Partitions()) + sage: M = MolecularSpecies("X,Y") + sage: A = M(G, {0: [5,6], 1: [1,2,3,4]}) sage: P = A.cycle_index(parent=tensor([F, F])) sage: P 1/4*B[[1, 1]] # B[[1, 1, 1, 1]] + 1/4*B[[1, 1]] # B[[2, 2]] + 1/4*B[[2]] # B[[1, 1, 1, 1]] + 1/4*B[[2]] # B[[2, 2]] @@ -1521,6 +1628,60 @@ def __call__(self, *args): return P(PermutationGroup(gens, domain=range(1, starts[-1] + 1)), pi, check=False) + def structures(self, *labels): + r""" + Iterate over the structures on the given set of labels. + + This yields a list of relabelled representatives of the + cosets of corresponding groups. + + The relabelling is such that the first few labels + correspond to the first factor in the atomic + decomposition, etc. + + EXAMPLES:: + + sage: from sage.rings.species import MolecularSpecies + sage: M = MolecularSpecies("X,Y") + sage: a = M(PermutationGroup([(3,4),(5,)]), {0:[1,3,4], 1:[2,5]}) + sage: a + X*Y^2*E_2(X) + sage: sorted(a.structures([1, 2, 3], ["a", "b"])) + [((1,), ('a',), ('b',), (2, 3)), + ((1,), ('b',), ('a',), (2, 3)), + ((2,), ('a',), ('b',), (1, 3)), + ((2,), ('b',), ('a',), (1, 3)), + ((3,), ('a',), ('b',), (1, 2)), + ((3,), ('b',), ('a',), (1, 2))] + + sage: G = PermutationGroup([[(2,3),(4,5)]]) + sage: a = M(G, {0: [1, 2, 3], 1: [4, 5]}) + sage: a + X*E_2(X*Y) + sage: sorted(a.structures([1, 2, 3], ["a", "b"])) + [((1,), (2, 3, 'a', 'b')), + ((1,), (2, 3, 'b', 'a')), + ((2,), (1, 3, 'a', 'b')), + ((2,), (1, 3, 'b', 'a')), + ((3,), (1, 2, 'a', 'b')), + ((3,), (1, 2, 'b', 'a'))] + """ + k = self.parent()._arity + labels = _label_sets(k, labels) + atoms = [a for a, n in self._monomial.items() for _ in range(n)] + sizes = [a._mc for a in atoms] + try: + # TODO: maybe OrderedSetPartitions should not raise + # an error if the second argument is a composition, + # but not of the right size + dissections = [OrderedSetPartitions(l, [mc[i] for mc in sizes]) + for i, l in enumerate(labels)] + except ValueError: + return + for d in product(*dissections): + yield from product(*[a.structures(*[l[i] for l in d]) + for i, a in enumerate(atoms)]) + class PolynomialSpeciesElement(CombinatorialFreeModule.Element): r""" @@ -1656,9 +1817,7 @@ def tilde(self): 2*E_2(X*Y) """ P = self.parent() - M = P._indices P_one = P.one() - one = ZZ.one() result = P.zero() for m, c in self: result_m = P_one @@ -2086,6 +2245,44 @@ def _from_etuple(e): for factor, exponent in factors] return Factorization(factors, unit=unit, sort=False) + def structures(self, *labels): + r""" + Iterate over the structures on the given set of labels. + + This yields a list of pairs consisting of a molecular species + and a relabelled representative of the cosets of + corresponding groups. + + The relabelling is such that the first few labels correspond + to the first factor in the atomic decomposition, etc. + + EXAMPLES:: + + sage: from sage.rings.species import PolynomialSpecies + sage: P = PolynomialSpecies(ZZ, ["X"]) + sage: C3 = P(CyclicPermutationGroup(3)) + sage: X = P(SymmetricGroup(1)) + sage: E2 = P(SymmetricGroup(2)) + sage: f = 2*E2*X + E2^2 + sage: list(f.structures([1, 2, 3])) + [(X*E_2, ((1, 3), (2,)), 0), + (X*E_2, ((2, 3), (1,)), 0), + (X*E_2, ((1, 2), (3,)), 0), + (X*E_2, ((1, 3), (2,)), 1), + (X*E_2, ((2, 3), (1,)), 1), + (X*E_2, ((1, 2), (3,)), 1)] + """ + labels = _label_sets(self.parent()._arity, labels) + for M, c in self.monomial_coefficients().items(): + if c not in ZZ or c < 0: + raise NotImplementedError("only implemented for proper non-virtual species") + if c == 1: + for s in M.structures(*labels): + yield M, s + else: + for e, s in cartesian_product([range(c), M.structures(*labels)]): + yield M, s, e + class PolynomialSpecies(CombinatorialFreeModule): r""" @@ -2254,7 +2451,6 @@ def _element_constructor_(self, G, pi=None, check=True): ValueError: 1/2 must be an element of the base ring, a permutation group or a pair (X, a) specifying a group action of the symmetric group on pi=None - """ if parent(G) is self: # pi cannot be None because of framework @@ -2477,6 +2673,25 @@ def _exponential(self, multiplicities, degrees): TESTS:: + sage: L = LazyCombinatorialSpecies(QQ, "X") + sage: E = L(lambda n: SymmetricGroup(n)) + sage: P = PolynomialSpecies(QQ, ["X"]) + + sage: c = 3/2; all((E^c)[i] == P._exponential([c], [i]) for i in range(6)) + True + + sage: c = -5/3; all((E^c)[i] == P._exponential([c], [i]) for i in range(6)) + True + + sage: c = 0; all((E^c)[i] == P._exponential([c], [i]) for i in range(6)) + True + + sage: c = 1; all((E^c)[i] == P._exponential([c], [i]) for i in range(6)) + True + + sage: c = -1; all((E^c)[i] == P._exponential([c], [i]) for i in range(6)) + True + sage: P = PolynomialSpecies(QQ, ["X"]) sage: P._exponential([1], [0]).parent() Polynomial species in X over Rational Field