From 6d7ab9c14e94bffa959e9c310ee3d16529006d77 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Tue, 18 Oct 2022 14:56:08 +0200 Subject: [PATCH 01/51] initial commit --- src/doc/en/reference/combinat/module_list.rst | 1 + src/sage/combinat/all.py | 4 + src/sage/combinat/bijectionist.py | 2720 +++++++++++++++++ 3 files changed, 2725 insertions(+) create mode 100644 src/sage/combinat/bijectionist.py diff --git a/src/doc/en/reference/combinat/module_list.rst b/src/doc/en/reference/combinat/module_list.rst index e42e3891d45..cee2984b1e0 100644 --- a/src/doc/en/reference/combinat/module_list.rst +++ b/src/doc/en/reference/combinat/module_list.rst @@ -22,6 +22,7 @@ Comprehensive Module List sage/combinat/alternating_sign_matrix sage/combinat/backtrack sage/combinat/baxter_permutations + sage/combinat/bijectionist sage/combinat/binary_recurrence_sequences sage/combinat/binary_tree sage/combinat/blob_algebra diff --git a/src/sage/combinat/all.py b/src/sage/combinat/all.py index e0450a1fbe3..af3af86a1a8 100644 --- a/src/sage/combinat/all.py +++ b/src/sage/combinat/all.py @@ -27,6 +27,7 @@ - :ref:`sage.combinat.designs.all` - :ref:`sage.combinat.posets.all` - :ref:`sage.combinat.words` +- :ref:`sage.combinat.bijectionist` Utilities --------- @@ -298,3 +299,6 @@ # Path Tableaux lazy_import('sage.combinat.path_tableaux', 'catalog', as_='path_tableaux') + +# Bijectionist +lazy_import('sage.combinat.bijectionist', 'Bijectionist') diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py new file mode 100644 index 00000000000..452a013b470 --- /dev/null +++ b/src/sage/combinat/bijectionist.py @@ -0,0 +1,2720 @@ +# -*- coding: utf-8 -*- +# pylint: disable=all + +# TODO: (high): +# +# check whether it makes sense to keep a list of solutions, and keep +# a global MILP up to date with this list + +# TODO: (medium): +# +# can we somehow tweak gurobi so that +# minimal_subdistributions_iterator considers the minimal +# subdistributions with "smallest" (e.g., in the sorting order +# defined above) elements first? + +r""" +A bijectionist's toolkit + +AUTHORS: + +- Alexander Grosz, Tobias Kietreiber, Stephan Pfannerer and Martin + Rubey (2020): Initial version + +Quick reference +=============== + +.. csv-table:: + :class: contentstable + :widths: 30, 70 + :delim: | + + :meth:`~Bijectionist.set_intertwining_relations` | Set + :meth:`~Bijectionist.set_constant_blocks` | Set + :meth:`~Bijectionist.set_statistics` | Set + :meth:`~Bijectionist.set_value_restrictions` | Set + :meth:`~Bijectionist.set_distributions` | Set + + :meth:`~Bijectionist.statistics_table` | Return + :meth:`~Bijectionist.statistics_fibers` | Return + + :meth:`~Bijectionist.constant_blocks` | Return + :meth:`~Bijectionist.solutions_iterator` | Return + :meth:`~Bijectionist.possible_values` | Return + :meth:`~Bijectionist.minimal_subdistributions_iterator` | Return + :meth:`~Bijectionist.minimal_subdistributions_blocks_iterator` | Return + +A guided tour +============= + + EXAMPLES: + + We find a statistic `s` such that + `(s, wex, fix) \sim (llis, des, adj)`:: + + sage: N = 3 + sage: As = [list(Permutations(n)) for n in range(N+1)] + sage: A = B = sum(As, []) + sage: alpha1 = lambda p: len(p.weak_excedences()) + sage: alpha2 = lambda p: len(p.fixed_points()) + sage: beta1 = lambda p: len(p.descents(final_descent=True)) if p else 0 + sage: beta2 = lambda p: len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1]) + sage: tau = Permutation.longest_increasing_subsequence_length + sage: def rotate_permutation(p): + ....: cycle = Permutation(tuple(range(1, len(p)+1))) + ....: return Permutation([cycle.inverse()(p(cycle(i))) for i in range(1, len(p)+1)]) + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_statistics((len, len), (alpha1, beta1), (alpha2, beta2)) + sage: a, b = bij.statistics_table() + sage: table(a, header_row=True, frame=True) + +-----------+--------+--------+--------+ + | a | α_1(a) | α_2(a) | α_3(a) | + +===========+========+========+========+ + | [] | 0 | 0 | 0 | + +-----------+--------+--------+--------+ + | [1] | 1 | 1 | 1 | + +-----------+--------+--------+--------+ + | [1, 2] | 2 | 2 | 2 | + +-----------+--------+--------+--------+ + | [2, 1] | 2 | 1 | 0 | + +-----------+--------+--------+--------+ + | [1, 2, 3] | 3 | 3 | 3 | + +-----------+--------+--------+--------+ + | [1, 3, 2] | 3 | 2 | 1 | + +-----------+--------+--------+--------+ + | [2, 1, 3] | 3 | 2 | 1 | + +-----------+--------+--------+--------+ + | [2, 3, 1] | 3 | 2 | 0 | + +-----------+--------+--------+--------+ + | [3, 1, 2] | 3 | 1 | 0 | + +-----------+--------+--------+--------+ + | [3, 2, 1] | 3 | 2 | 1 | + +-----------+--------+--------+--------+ + + sage: table(b, header_row=True, frame=True) + +-----------+---+--------+--------+--------+ + | b | τ | β_1(b) | β_2(b) | β_3(b) | + +===========+===+========+========+========+ + | [] | 0 | 0 | 0 | 0 | + +-----------+---+--------+--------+--------+ + | [1] | 1 | 1 | 1 | 1 | + +-----------+---+--------+--------+--------+ + | [1, 2] | 2 | 2 | 1 | 0 | + +-----------+---+--------+--------+--------+ + | [2, 1] | 1 | 2 | 2 | 2 | + +-----------+---+--------+--------+--------+ + | [1, 2, 3] | 3 | 3 | 1 | 0 | + +-----------+---+--------+--------+--------+ + | [1, 3, 2] | 2 | 3 | 2 | 1 | + +-----------+---+--------+--------+--------+ + | [2, 1, 3] | 2 | 3 | 2 | 1 | + +-----------+---+--------+--------+--------+ + | [2, 3, 1] | 2 | 3 | 2 | 1 | + +-----------+---+--------+--------+--------+ + | [3, 1, 2] | 2 | 3 | 2 | 0 | + +-----------+---+--------+--------+--------+ + | [3, 2, 1] | 1 | 3 | 3 | 3 | + +-----------+---+--------+--------+--------+ + + sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition + sage: bij.set_constant_blocks(sum([orbit_decomposition(A, rotate_permutation) for A in As], [])) + sage: bij.constant_blocks() + {{[1, 3, 2], [2, 1, 3], [3, 2, 1]}} + sage: next(bij.solutions_iterator()) + {[]: 0, + [1]: 1, + [1, 2]: 1, + [1, 2, 3]: 1, + [1, 3, 2]: 2, + [2, 1]: 2, + [2, 1, 3]: 2, + [2, 3, 1]: 2, + [3, 1, 2]: 3, + [3, 2, 1]: 2} + + There is no rotation invariant statistic on non crossing set partitions which is equidistributed + with the Strahler number on ordered trees:: + + sage: N=8; As = [[SetPartition(d.to_noncrossing_partition()) for d in DyckWords(n)] for n in range(N)] + sage: A = sum(As, []) + sage: B = sum([list(OrderedTrees(n)) for n in range(1, N+1)], []) + sage: theta = lambda m: SetPartition([[i % m.size() + 1 for i in b] for b in m]) + + The following code is equivalent to ``tau = findstat(397)``:: + + sage: def tau(T): + ....: if len(T) == 0: + ....: return 1 + ....: else: + ....: l = [tau(S) for S in T] + ....: m = max(l) + ....: if l.count(m) == 1: + ....: return m + ....: else: + ....: return m+1 + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_statistics((lambda a: a.size(), lambda b: b.node_number()-1)) + sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition + sage: bij.set_constant_blocks(sum([orbit_decomposition(A_n, theta) for A_n in As], [])) + sage: list(bij.solutions_iterator()) + [] + + An example identifying `s` and `S`:: + + sage: N = 4 + sage: A = [dyck_word for n in range(1, N) for dyck_word in DyckWords(n)] + sage: B = [binary_tree for n in range(1, N) for binary_tree in BinaryTrees(n)] + sage: concat_path = lambda D1, D2: DyckWord(list(D1) + list(D2)) + sage: concat_tree = lambda B1, B2: concat_path(B1.to_dyck_word(), + ....: B2.to_dyck_word()).to_binary_tree() + sage: bij = Bijectionist(A, B) + sage: bij.set_intertwining_relations((2, concat_path, concat_tree)) + sage: bij.set_statistics((lambda d: d.semilength(), lambda t: t.node_number())) + sage: for D in bij.minimal_subdistributions_iterator(): + ....: ascii_art(D) + ( [ /\ ], [ o ] ) + ( [ o ] ) + ( [ \ ] ) + ( [ /\/\ ], [ o ] ) + ( [ o ] ) + ( [ /\ ] [ / ] ) + ( [ / \ ], [ o ] ) + ( [ o ] ) + ( [ \ ] ) + ( [ o ] ) + ( [ \ ] ) + ( [ /\/\/\ ], [ o ] ) + ( [ o ] ) + ( [ \ ] ) + ( [ o ] ) + ( [ /\ ] [ / ] ) + ( [ /\/ \ ], [ o ] ) + ( [ o ] ) + ( [ /\ ] [ / \ ] ) + ( [ / \/\ ], [ o o ] ) + ( [ o, o ] ) + ( [ / / ] ) + ( [ /\ ] [ o o ] ) + ( [ /\/\ / \ ] [ \ / ] ) + ( [ / \, / \ ], [ o o ] ) + + TESTS: + + The following failed before commit c6d4d2e8804aa42afa08c72c887d50c725cc1a91:: + + sage: N=4; A = B = [permutation for n in range(N) for permutation in Permutations(n)] + sage: theta = lambda pi: Permutation([x+1 if x != len(pi) else 1 for x in pi[-1:]+pi[:-1]]) + sage: def tau(pi): + ....: n = len(pi) + ....: return sum([1 for i in range(1, n+1) for j in range(1, n+1) + ....: if i +# Stephan Pfannerer +# Tobias Kietreiber +# Alexander Grosz +# +# Distributed under the terms of the GNU General Public License (GPL) +# +# This code is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# The full text of the GPL is available at: +# +# https://www.gnu.org/licenses/ +# *************************************************************************** +import itertools +from collections import namedtuple +from sage.numerical.mip import MixedIntegerLinearProgram, MIPSolverException +from sage.rings.integer_ring import ZZ +from sage.combinat.set_partition import SetPartition +from sage.sets.disjoint_set import DisjointSet +from sage.structure.sage_object import SageObject +from copy import copy, deepcopy +from sage.misc.verbose import get_verbose + +# TODO: (low) für sagemath sollten wir Zeilen möglichst auf 79 +# Zeichen beschränken, das ist zwar nicht streng, wird aber lieber +# gesehen. + +# TODO: (low) we frequently need variable names for subsets of A, B, +# Z. In LaTeX, we mostly call them \tilde A, \tilde Z, etc. now. It +# would be good to have a standard name in code, too. + +# TODO: (medium) wann immer möglich, sollten die Tests in einer +# Methode nur diese eine Methode testen. Wir haben in fast allen +# Methoden "system tests", das ist unpraktisch, wenn man größere +# Änderungen durchführt. + + +class Bijectionist(SageObject): + r"""Solver class for bijection-statistic problems. + + INPUT: + + - ``A``, ``B`` -- sets of equal size, given as a list + + - ``tau`` (optional, default: ``None``) -- a function from ``B`` + to ``Z``, in case of ``None``, the identity map ``lambda x: x`` + is used + + - ``alpha`` (optional) -- a statistic from ``A`` to ``W`` + + - ``beta`` (optional) -- a statistic from ``B`` to ``W`` + + - ``P`` (optional) -- a partition of ``A`` + + - ``pi_rho`` (optional) -- a triple ``(k, pi, rho)`` where + + - ``pi`` is a ``k``-ary operation composing objects in ``A`` + and + + - ``rho`` is a ``k``-ary function composing statistic values + in `Z` + + ``W`` and ``Z`` can be arbitrary sets. As a natural example we + may think of the natural numbers or tuples of integers. + + We are looking for a statistic `s: A\to Z` and a bijection `S: + A\to B` such that + + - `s = \tau \circ S`: the statistics `s` and `\tau` are + equidistributed and `S` is an intertwining bijection. + + - `\alpha = \beta \circ S`: the statistics `\alpha` and `\beta` + are equidistributed and `S` is an intertwining bijection. + + - `s` is constant on the blocks of `P`. + + - `s(\pi(a_1,\dots, a_k)) = \rho(s(a_1),\dots, s(a_k))`. + + Additionally, we may require that + + - `s(a)\in Z_a` for specified sets `Z_a\subseteq Z`, and + + - `s|_{\tilde A}` has a specified distribution for specified sets + `\tilde A \subset A`. + + If `\tau` is the identity, the two unknown functions `s` and `S` + coincide. Although we do not exclude other bijective choices for + `\tau`, they probably do not make sense. + + If we want that `S` is graded, i.e. if elements of `A` and `B` + have a notion of size and `S` should preserve this size, we can + add grading statistics as `\alpha` and `\beta`. Since `\alpha` + and `\beta` will be equidistributed with `S` as an intertwining + bijection, `S` will then also be graded. + + In summary, we have the following two commutative diagrams, where + `s` and `S` are unknown functions. + + .. MATH:: + + \begin{array}{rrl} + & A \\ + {\scriptstyle\alpha}\swarrow & {\scriptstyle S}\downarrow & \searrow{\scriptstyle s}\\ + W \overset{\beta}{\leftarrow} & B & \overset{\tau}{\rightarrow} Z + \end{array} + \qquad + \begin{array}{lcl} + A^k &\overset{\pi}{\rightarrow} & A\\ + \downarrow{\scriptstyle s^k} & & \downarrow{\scriptstyle s}\\ + Z^k &\overset{\rho}{\rightarrow} & Z\\ + \end{array} + + .. NOTE:: + + If `\tau` is the identity map, the partition `P` of `A` + necessarily consists only of singletons. + + .. NOTE:: + + The order of invocation of the methods with prefix ``set``, + i.e., :meth:`set_statistics`, + :meth:`set_intertwining_relations`, + :meth:`set_constant_blocks`, etc., is irrelevant. Calling + any of these methods a second time overrides the previous + specification. + + """ + def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], pi_rho=tuple(), elements_distributions=tuple(), a_values=tuple(), solver=None, key=None): + # glossary of standard letters: + # A, B, Z, W ... finite sets + # ???? tilde_A, tilde_Z, ..., subsets? + # P ... set partition of A + # a in A, b in B, p in P + # S: A -> B + # alpha: A -> W, beta: B -> W + # s: A -> Z, tau: B -> Z + # k arity of pi and rho + # pi: A^k -> A, rho: Z^k -> Z + # a_tuple in A^k + + assert len(A) == len(set(A)), "A must have distinct items" + assert len(B) == len(set(B)), "B must have distinct items" + self._A = A + self._B = B + self._sorter = {} + self._sorter["A"] = lambda x: sorted(x, key=self._A.index) + self._sorter["B"] = lambda x: sorted(x, key=self._B.index) + + if tau is None: + self._tau = {b: b for b in self._B} + else: + self._tau = {b: tau(b) for b in self._B} + self._Z = set(self._tau.values()) + if key is not None and "Z" in key: + self._sorter["Z"] = lambda x: sorted(x, key=key["Z"]) + self._Z = self._sorter["Z"](self._Z) + else: + try: + self._Z = sorted(self._Z) + self._sorter["Z"] = lambda x: sorted(x) + except TypeError: + self._sorter["Z"] = lambda x: list(x) + self._Z = list(self._Z) + + # set optional inputs + self.set_statistics(*alpha_beta) + self.set_value_restrictions(*a_values) + self.set_distributions(*elements_distributions) + self.set_intertwining_relations(*pi_rho) + self.set_constant_blocks(P) + + self._solver = solver + + def set_constant_blocks(self, P): + r""" + Declare that `s: A\to Z` is constant on each block of `P`. + + .. WARNING:: + + Any restriction imposed by a previous invocation of + :meth:`set_constant_blocks` will be overwritten, + including restrictions discovered by + :meth:`set_intertwining_relations` and + :meth:`solutions_iterator`! + + A common example is to use the orbits of a bijection acting + on `A`. This can be achieved using the function + :meth:`~sage.combinat.cyclic_sieving_phenomenon.orbit_decomposition`. + + INPUT: + + - ``P`` -- a set partition of `A`, singletons may be omitted + + EXAMPLES: + + Initially the partitions are set to singleton blocks. The + current partition can be reviewed using + :meth:`constant_blocks`:: + + sage: A = B = list('abcd') + sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2) + sage: bij.constant_blocks() + {} + + sage: bij.set_constant_blocks([['a', 'c']]) + sage: bij.constant_blocks() + {{'a', 'c'}} + + We now add a map that combines some blocks:: + + sage: pi = lambda p1, p2: 'abcdefgh'[A.index(p1) + A.index(p2)] + sage: rho = lambda s1, s2: (s1 + s2) % 2 + sage: bij.set_intertwining_relations((2, pi, rho)) + sage: list(bij.solutions_iterator()) + [{'a': 0, 'b': 1, 'c': 0, 'd': 1}] + sage: bij.constant_blocks() + {{'a', 'c'}, {'b', 'd'}} + + Setting constant blocks overrides any previous assignment:: + + sage: bij.set_constant_blocks([['a', 'b']]) + sage: bij.constant_blocks() + {{'a', 'b'}} + + If there is no solution, and the coarsest partition is + requested, an error is raised:: + + sage: bij.constant_blocks(optimal=True) + Traceback (most recent call last): + ... + MIPSolverException: ... + + """ + self._P = DisjointSet(self._A) + P = sorted(self._sorter["A"](p) for p in P) + for p in P: + for a in p: + self._P.union(p[0], a) + + self._compute_possible_block_values() + + def constant_blocks(self, singletons=False, optimal=False): + r""" + Return the set partition `P` of `A` such that `s: A\to Z` is + known to be constant on the blocks of `P`. + + INPUT: + + - ``singletons`` (optional, default: ``False``) -- whether or + not to include singleton blocks in the output + + - ``optimal`` (optional, default: ``False``) -- whether or + not to compute the coarsest possible partition + + .. NOTE:: + + computing the coarsest possible partition may be + computationally expensive, but may speed up generating + solutions. + + EXAMPLES:: + + sage: A = B = ["a", "b", "c"] + sage: bij = Bijectionist(A, B, lambda x: 0) + sage: bij.set_constant_blocks([["a", "b"]]) + sage: bij.constant_blocks() + {{'a', 'b'}} + + sage: bij.constant_blocks(singletons=True) + {{'a', 'b'}, {'c'}} + + """ + if optimal: + self._forced_constant_blocks() + if singletons: + return SetPartition(self._P) + return SetPartition(p for p in self._P if len(p) > 1) + + def set_statistics(self, *alpha_beta): + r""" + Set constraints of the form `\alpha = \beta\circ S`. + + .. WARNING:: + + Any restriction imposed by a previous invocation of + :meth:`set_statistics` will be overwritten! + + INPUT: + + - ``alpha_beta`` -- one or more pairs `(\alpha: A\to W, + \beta: B\to W)` + + If the statistics `\alpha` and `\beta` are not + equidistributed, an error is raised. + + EXAMPLES: + + We look for bijections `S` on permutations such that the + number of weak exceedences of `S(\pi)` equals the number of + descents of `\pi`, and statistics `s`, such that the number + of fixed points of `S(\pi)` equals `s(\pi)`:: + + sage: N = 4; A = B = [permutation for n in range(N) for permutation in Permutations(n)] + sage: wex = lambda p: len(p.weak_excedences()) + sage: fix = lambda p: len(p.fixed_points()) + sage: des = lambda p: len(p.descents(final_descent=True)) if p else 0 + sage: adj = lambda p: len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1]) + sage: bij = Bijectionist(A, B, fix) + sage: bij.set_statistics((wex, des), (len, len)) + sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): + ....: print(solution) + {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 0, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 3, [3, 2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 0, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 3, [3, 2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 0, [2, 1, 3]: 1, [2, 3, 1]: 1, [3, 1, 2]: 3, [3, 2, 1]: 0} + {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 1, [2, 1, 3]: 0, [2, 3, 1]: 0, [3, 1, 2]: 3, [3, 2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 1, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 3, [3, 2, 1]: 0} + {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 3, [3, 2, 1]: 0} + + sage: bij = Bijectionist(A, B, fix) + sage: bij.set_statistics((wex, des), (fix, adj), (len, len)) + sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): + ....: print(solution) + {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 0, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 3, [3, 2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 1, [2, 1, 3]: 0, [2, 3, 1]: 0, [3, 1, 2]: 3, [3, 2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 3, [3, 2, 1]: 0} + + Calling this with non-equidistributed statistics yields an error:: + + sage: bij = Bijectionist(A, B, fix) + sage: bij.set_statistics((wex, fix)) + Traceback (most recent call last): + ... + ValueError: Statistics alpha and beta are not equidistributed! + + TESTS: + + Calling ``set_statistics`` without arguments should restore the previous state.:: + + sage: N = 3; A = B = [permutation for n in range(N) for permutation in Permutations(n)] + sage: wex = lambda p: len(p.weak_excedences()) + sage: fix = lambda p: len(p.fixed_points()) + sage: des = lambda p: len(p.descents(final_descent=True)) if p else 0 + sage: bij = Bijectionist(A, B, fix) + sage: bij.set_statistics((wex, des), (len, len)) + sage: for solution in bij.solutions_iterator(): + ....: print(solution) + {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2} + sage: bij.set_statistics() + sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): + ....: print(solution) + {[]: 0, [1]: 0, [1, 2]: 1, [2, 1]: 2} + {[]: 0, [1]: 0, [1, 2]: 2, [2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0} + {[]: 0, [1]: 2, [1, 2]: 0, [2, 1]: 1} + {[]: 0, [1]: 2, [1, 2]: 1, [2, 1]: 0} + {[]: 1, [1]: 0, [1, 2]: 0, [2, 1]: 2} + {[]: 1, [1]: 0, [1, 2]: 2, [2, 1]: 0} + {[]: 1, [1]: 2, [1, 2]: 0, [2, 1]: 0} + {[]: 2, [1]: 0, [1, 2]: 0, [2, 1]: 1} + {[]: 2, [1]: 0, [1, 2]: 1, [2, 1]: 0} + {[]: 2, [1]: 1, [1, 2]: 0, [2, 1]: 0} + + """ + # reset values + self._statistics_possible_values = {a: set(self._Z) for a in self._A} + + self._n_statistics = len(alpha_beta) + # TODO: (low) do we really want to recompute statistics every time? + self._alpha = lambda p: tuple(arg[0](p) for arg in alpha_beta) + self._beta = lambda p: tuple(arg[1](p) for arg in alpha_beta) + + # generate fibers + self._statistics_fibers = {} + for a in self._A: + v = self._alpha(a) + if v not in self._statistics_fibers: + self._statistics_fibers[v] = ([], []) + self._statistics_fibers[v][0].append(a) + + for b in self._B: + v = self._beta(b) + if v not in self._statistics_fibers: + raise ValueError(f"Statistics alpha and beta do not have the same image, {v} is not a value of alpha, but of beta!") + self._statistics_fibers[v][1].append(b) + + # check compatibility + if not all(len(fiber[0]) == len(fiber[1]) + for fiber in self._statistics_fibers.values()): + raise ValueError("Statistics alpha and beta are not equidistributed!") + + self._W = list(self._statistics_fibers) + + # the possible values of s(a) are tau(beta^{-1}(alpha(a))) + self._statistics_possible_values = {a: set(self._tau[b] + for b in self._statistics_fibers[self._alpha(a)][1]) + for a in self._A} + + def statistics_fibers(self): + r""" + Return a dictionary mapping statistic values in `W` to their + preimages in `A` and `B`. + + This is a (computationally) fast way to obtain a first + impression which objects in `A` should be mapped to which + objects in `B`. + + EXAMPLES:: + + sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] + sage: tau = Permutation.longest_increasing_subsequence_length + sage: wex = lambda p: len(p.weak_excedences()) + sage: fix = lambda p: len(p.fixed_points()) + sage: des = lambda p: len(p.descents(final_descent=True)) if p else 0 + sage: adj = lambda p: len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1]) + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_statistics((len, len), (wex, des), (fix, adj)) + sage: table([[key, AB[0], AB[1]] for key, AB in bij.statistics_fibers().items()]) + (0, 0, 0) [[]] [[]] + (1, 1, 1) [[1]] [[1]] + (2, 2, 2) [[1, 2]] [[2, 1]] + (2, 1, 0) [[2, 1]] [[1, 2]] + (3, 3, 3) [[1, 2, 3]] [[3, 2, 1]] + (3, 2, 1) [[1, 3, 2], [2, 1, 3], [3, 2, 1]] [[1, 3, 2], [2, 1, 3], [2, 3, 1]] + (3, 2, 0) [[2, 3, 1]] [[3, 1, 2]] + (3, 1, 0) [[3, 1, 2]] [[1, 2, 3]] + + """ + return self._statistics_fibers + + def statistics_table(self, header=True): + r""" + Provide information about all elements of `A` with corresponding + `\alpha` values and all elements of `B` with corresponding + `\beta` and `\tau` values. + + INPUT: + + - ``header`` (optional, default: ``True``) -- whether to + include a header with the standard greek letters. + + OUTPUT: + + A pair of lists suitable for :class:`~sage.misc.table.table`, + where + + - the first contains the elements of `A` together with the + values of `\alpha` + + - the second contains the elements of `B` together with the + values of `\tau` and `\beta` + + EXAMPLES:: + + sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] + sage: tau = Permutation.longest_increasing_subsequence_length + sage: wex = lambda p: len(p.weak_excedences()) + sage: fix = lambda p: len(p.fixed_points()) + sage: des = lambda p: len(p.descents(final_descent=True)) if p else 0 + sage: adj = lambda p: len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1]) + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_statistics((wex, des), (fix, adj)) + sage: a, b = bij.statistics_table() + sage: table(a, header_row=True, frame=True) + +-----------+--------+--------+ + | a | α_1(a) | α_2(a) | + +===========+========+========+ + | [] | 0 | 0 | + +-----------+--------+--------+ + | [1] | 1 | 1 | + +-----------+--------+--------+ + | [1, 2] | 2 | 2 | + +-----------+--------+--------+ + | [2, 1] | 1 | 0 | + +-----------+--------+--------+ + | [1, 2, 3] | 3 | 3 | + +-----------+--------+--------+ + | [1, 3, 2] | 2 | 1 | + +-----------+--------+--------+ + | [2, 1, 3] | 2 | 1 | + +-----------+--------+--------+ + | [2, 3, 1] | 2 | 0 | + +-----------+--------+--------+ + | [3, 1, 2] | 1 | 0 | + +-----------+--------+--------+ + | [3, 2, 1] | 2 | 1 | + +-----------+--------+--------+ + sage: table(b, header_row=True, frame=True) + +-----------+---+--------+--------+ + | b | τ | β_1(b) | β_2(b) | + +===========+===+========+========+ + | [] | 0 | 0 | 0 | + +-----------+---+--------+--------+ + | [1] | 1 | 1 | 1 | + +-----------+---+--------+--------+ + | [1, 2] | 2 | 1 | 0 | + +-----------+---+--------+--------+ + | [2, 1] | 1 | 2 | 2 | + +-----------+---+--------+--------+ + | [1, 2, 3] | 3 | 1 | 0 | + +-----------+---+--------+--------+ + | [1, 3, 2] | 2 | 2 | 1 | + +-----------+---+--------+--------+ + | [2, 1, 3] | 2 | 2 | 1 | + +-----------+---+--------+--------+ + | [2, 3, 1] | 2 | 2 | 1 | + +-----------+---+--------+--------+ + | [3, 1, 2] | 2 | 2 | 0 | + +-----------+---+--------+--------+ + | [3, 2, 1] | 1 | 3 | 3 | + +-----------+---+--------+--------+ + + TESTS: + + If no statistics are given, the table should still be able to be generated:: + + sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)] + sage: tau = Permutation.longest_increasing_subsequence_length + sage: bij = Bijectionist(A, B, tau) + sage: a, b = bij.statistics_table() + sage: table(a, header_row=True, frame=True) + +--------+ + | a | + +========+ + | [] | + +--------+ + | [1] | + +--------+ + | [1, 2] | + +--------+ + | [2, 1] | + +--------+ + sage: table(b, header_row=True, frame=True) + +--------+---+ + | b | τ | + +========+===+ + | [] | 0 | + +--------+---+ + | [1] | 1 | + +--------+---+ + | [1, 2] | 2 | + +--------+---+ + | [2, 1] | 1 | + +--------+---+ + + We can omit the header:: + + sage: bij.statistics_table(header=True)[1] + [['b', 'τ'], [[], 0], [[1], 1], [[1, 2], 2], [[2, 1], 1]] + sage: bij.statistics_table(header=False)[1] + [[[], 0], [[1], 1], [[1, 2], 2], [[2, 1], 1]] + + """ + # table for alpha + n_statistics = self._n_statistics + if header: + output_alphas = [["a"] + ["\u03b1_" + str(i) + "(a)" + for i in range(1, n_statistics + 1)]] + else: + output_alphas = [] + + for a in self._A: + if n_statistics > 0: + output_alphas.append([a] + list(self._alpha(a))) + else: + output_alphas.append([a]) + + # table for beta and tau + if header: + output_tau_betas = [["b", "\u03c4"] + ["\u03b2_" + str(i) + "(b)" + for i in range(1, n_statistics + 1)]] + else: + output_tau_betas = [] + for b in self._B: + if n_statistics > 0: + output_tau_betas.append([b, self._tau[b]] + list(self._beta(b))) + else: + output_tau_betas.append([b, self._tau[b]]) + + return output_alphas, output_tau_betas + + def set_value_restrictions(self, *a_values): + r""" + Restrict the set of possible values `s(a)` for a given element + `a`. + + .. WARNING:: + + Any restriction imposed by a previous invocation of + :meth:`set_value_restrictions` will be overwritten! + + INPUT: + + - ``a_values`` -- one or more pairs `(a\in A, \tilde + Z\subseteq Z)` + + EXAMPLES: + + We may want to restrict the value of a given element to a + single or multiple values. We do not require that the + specified values are in the image of `\tau`. In some + cases, the restriction may not be able to provide a better + solution, as for size 3 in the following example. :: + + sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] + sage: tau = Permutation.longest_increasing_subsequence_length + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_statistics((len, len)) + sage: bij.set_value_restrictions((Permutation([1, 2]), [1]), + ....: (Permutation([3, 2, 1]), [2, 3, 4])) + sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): + ....: print(sol) + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 3} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 3, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 3, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 2, [2, 1, 3]: 3, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 3} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 3, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 3, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 1, [2, 1, 3]: 3, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 3} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 1, [2, 3, 1]: 2, [3, 1, 2]: 3, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 1, [2, 3, 1]: 3, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 1, [3, 1, 2]: 2, [3, 2, 1]: 3} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 1, [3, 1, 2]: 3, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 3} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 3, [3, 1, 2]: 1, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 3, [2, 3, 1]: 1, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 3, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 1, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 1, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2} + + However, an error occurs if the set of possible values is + empty. In this example, the image of `\tau` under any + legal bijection is disjoint to the specified values. :: TODO: we now have to call _compute_possible_block_values() for the error message. Is this intended behaviour? + + sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] + sage: tau = Permutation.longest_increasing_subsequence_length + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_value_restrictions((Permutation([1, 2]), [4, 5])) + sage: bij._compute_possible_block_values() + Traceback (most recent call last): + ... + ValueError: No possible values found for singleton block [[1, 2]] + + TESTS:: + + sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] + sage: tau = Permutation.longest_increasing_subsequence_length + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_constant_blocks([[permutation for permutation in Permutations(n)] for n in range(4)]) + sage: bij.set_value_restrictions((Permutation([1, 2]), [4, 5])) + sage: bij._compute_possible_block_values() + Traceback (most recent call last): + ... + ValueError: No possible values found for block [[1, 2], [2, 1]] + + sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] + sage: tau = Permutation.longest_increasing_subsequence_length + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_value_restrictions(((1, 2), [4, 5, 6])) + Traceback (most recent call last): + ... + AssertionError: Element (1, 2) was not found in A + + """ + # reset values + self._restrictions_possible_values = {a: set(self._Z) for a in self._A} + for a, values in a_values: + assert a in self._A, f"Element {a} was not found in A" + self._restrictions_possible_values[a].intersection_update(values) + + def _compute_possible_block_values(self): + r""" + Update the dictionary of possible values of each block. + """ + self._possible_block_values = {} # P -> Power(Z) + for p, block in self._P.root_to_elements_dict().items(): + self._possible_block_values[p] = set.intersection(*[self._restrictions_possible_values[a] for a in block], *[self._statistics_possible_values[a] for a in block]) + if not self._possible_block_values[p]: + if len(block) == 1: + raise ValueError(f"No possible values found for singleton block {block}") + else: + raise ValueError(f"No possible values found for block {block}") + + def set_distributions(self, *elements_distributions): + r""" + Specify the distribution of `s` for a subset of elements. + + .. WARNING:: + + Any restriction imposed by a previous invocation of + :meth:`set_distributions` will be overwritten! + + INPUT: + + - one or more pairs of `(\tilde A\subseteq A, \tilde Z)`, + where `\tilde Z` is a list of values in `Z` of the same + size as `\tilde A` + + This method specifies that `\{s(a) | a\in\tilde A\}` equals + ``\tilde Z`` as a multiset for each of the pairs. + + When specifying several distributions, the subsets of `A` do + not have to be disjoint. + + EXAMPLES:: + + sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] + sage: tau = Permutation.longest_increasing_subsequence_length + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_statistics((len, len)) + sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [1, 3])) + sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): + ....: print(sol) + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 1, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2} + + sage: bij.constant_blocks(optimal=True) + {{[2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]}} + sage: sorted(bij.minimal_subdistributions_blocks_iterator(), key=lambda d: (len(d[0]), d[0])) + [([[]], [0]), + ([[1]], [1]), + ([[2, 1, 3]], [2]), + ([[1, 2], [2, 1]], [1, 2]), + ([[1, 2, 3], [1, 3, 2]], [1, 3])] + + We may also specify multiple, possibly overlapping distributions:: + + sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [1, 3]), + ....: ([Permutation([1, 3, 2]), Permutation([3, 2, 1]), + ....: Permutation([2, 1, 3])], [1, 2, 2])) + sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): + ....: print(sol) + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2} + + sage: bij.constant_blocks(optimal=True) + {{[1], [1, 3, 2]}, {[2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]}} + sage: sorted(bij.minimal_subdistributions_blocks_iterator(), key=lambda d: (len(d[0]), d[0])) + [([[]], [0]), + ([[1]], [1]), + ([[1, 2, 3]], [3]), + ([[2, 1, 3]], [2]), + ([[1, 2], [2, 1]], [1, 2])] + + TESTS: + + Because of the current implementation of the output + calculation, we do not improve our solution if we do not gain + any unique solutions.:: + + sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] + sage: tau = Permutation.longest_increasing_subsequence_length + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_statistics((len, len)) + sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [2, 3])) + sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): + ....: print(sol) + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 1, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 1, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 1, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 1, [3, 1, 2]: 2, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1} + + Another example with statistics:: + + sage: bij = Bijectionist(A, B, tau) + sage: alpha = lambda p: p(1) if len(p) > 0 else 0 + sage: beta = lambda p: p(1) if len(p) > 0 else 0 + sage: bij.set_statistics((alpha, beta), (len, len)) + sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): + ....: print(sol) + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1} + + The solution above is not unique. We can add a feasible distribution to force uniqueness:: + + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_statistics((alpha, beta), (len, len)) + sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([3, 2, 1])], [1, 3])) + sage: for sol in bij.solutions_iterator(): + ....: print(sol) + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1} + + Let us try to add a distribution that cannot be satisfied, + because there is no solution where a permutation that starts + with 1 is mapped onto 1:: + + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_statistics((alpha, beta), (len, len)) + sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [1, 3])) + sage: list(bij.solutions_iterator()) + [] + + The specified elements have to be in `A` and have to be of the same size:: + + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_statistics((len, len)) + sage: bij.set_distributions(([Permutation([1, 2, 3, 4])], [1])) + Traceback (most recent call last): + ... + ValueError: Element [1, 2, 3, 4] was not found in A! + sage: bij.set_distributions(([Permutation([1, 2, 3])], [-1])) + Traceback (most recent call last): + ... + ValueError: Value -1 was not found in tau(A)! + + Note that the same error occurs when an element that is not the first element of the list is + not in `A`. + + """ + for elements, values in elements_distributions: + assert len(elements) == len(values), f"{elements} and {values} are not of the same size!" + for a, z in zip(elements, values): + if a not in self._A: + raise ValueError(f"Element {a} was not found in A!") + if z not in self._Z: + raise ValueError(f"Value {z} was not found in tau(A)!") + self._elements_distributions = elements_distributions + + def set_intertwining_relations(self, *pi_rho): + r""" + Add restrictions of the form `s(\pi(a_1,\dots, a_k)) = + \rho(s(a_1),\dots, s(a_k))`. + + .. WARNING:: + + Any restriction imposed by a previous invocation of + :meth:`set_intertwining_relations` will be overwritten! + + INPUT: + + - ``pi_rho`` -- one or more tuples `(k, \pi: A^k\to A, \rho: + Z^k\to Z, \tilde A)` where `\tilde A` (optional) is an + `k`-ary function that returns true if and only if an + `k`-tuple of objects in `A` is in the domain of `\pi` + + EXAMPLES: + + We can concatenate two permutations, by increasing the values + of the second permutation by the length of the first + permutation:: + + sage: concat = lambda p1, p2: Permutation(p1 + [i + len(p1) for i in p2]) + + We may be interested in statistics on permutations which are + equidistributed with the number of fixed points, such that + concatenating permutations corresponds to adding statistic + values:: + + sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] + sage: bij = Bijectionist(A, B, Permutation.number_of_fixed_points) + sage: bij.set_statistics((len, len)) + sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): + ....: print(solution) + ... + {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 0, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 1, [3, 2, 1]: 3} + ... + {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 1, [2, 1, 3]: 3, [2, 3, 1]: 0, [3, 1, 2]: 0, [3, 2, 1]: 1} + ... + + sage: bij.set_intertwining_relations((2, concat, lambda x, y: x + y)) + sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): + ....: print(solution) + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 0, [3, 2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 1, [3, 2, 1]: 0} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 1, [3, 1, 2]: 0, [3, 2, 1]: 0} + + The domain of the composition may be restricted. E.g., if we + concatenate only permutations starting with a 1, we obtain + fewer forced elements:: + + sage: in_domain = lambda p1, p2: (not p1 or p1(1) == 1) and (not p2 or p2(1) == 1) + sage: bij.set_intertwining_relations((2, concat, lambda x, y: x + y, in_domain)) + sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): + ....: print(solution) + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 0, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 1, [3, 2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 0, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 1, [3, 2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 0, [2, 1, 3]: 1, [2, 3, 1]: 1, [3, 1, 2]: 0, [3, 2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 0, [2, 1, 3]: 1, [2, 3, 1]: 1, [3, 1, 2]: 1, [3, 2, 1]: 0} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 0, [2, 3, 1]: 0, [3, 1, 2]: 1, [3, 2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 0, [3, 2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 1, [3, 2, 1]: 0} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 0, [3, 2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 1, [3, 2, 1]: 0} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 1, [3, 1, 2]: 0, [3, 2, 1]: 0} + + We can also restrict according to several composition + functions. For example, we may additionally concatenate + permutations by incrementing the elements of the first:: + + sage: skew_concat = lambda p1, p2: Permutation([i + len(p2) for i in p1] + list(p2)) + sage: bij.set_intertwining_relations((2, skew_concat, lambda x, y: x + y)) + sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): + ....: print(solution) + {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 0, [1, 3, 2]: 0, [2, 1, 3]: 1, [2, 3, 1]: 1, [3, 1, 2]: 1, [3, 2, 1]: 3} + {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 0, [1, 3, 2]: 1, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 1, [3, 2, 1]: 3} + {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 0, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 1, [3, 2, 1]: 3} + + However, this yields no solution:: + + sage: bij.set_intertwining_relations((2, concat, lambda x, y: x + y), (2, skew_concat, lambda x, y: x + y)) + sage: list(bij.solutions_iterator()) + [] + + """ + Pi_Rho = namedtuple("Pi_Rho", "numargs pi rho domain") + self._pi_rho = [] + + for pi_rho_tuple in pi_rho: + if len(pi_rho_tuple) == 3: + k, pi, rho = pi_rho_tuple + domain = None + else: + k, pi, rho, domain = pi_rho_tuple + + self._pi_rho.append(Pi_Rho(numargs=k, pi=pi, rho=rho, domain=domain)) + + def _forced_constant_blocks(self): + r""" + Modify current partition into blocks to the coarsest possible + one, meaning that after calling this function for every two + distinct blocks `p_1`, `p_2` there exists a solution `s` with + `s(p_1)\neq s(p_2)`. + + ALGORITHM: + + First we generate an initial solution. For all blocks i, j + that have the same value under this initial solution, we add + the constraint `x[i, z] + x[j, z] <= 1` for all possible + values `z\in Z`. This constraint ensures that the `s` differs + on the two blocks. If this modified problem does not have a + solution, we know that the two blocks always have the same + value and join them. Then we save all values of this new + solution and continue looking at pairs of blocks that had the + same value under all calculated solutions, until no blocks + can be joined anymore. + + EXAMPLES: + + The easiest example is given by a constant `tau`, so everything + is forced to be the same value: + + sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)] + sage: bij = Bijectionist(A, B, lambda x: 0) + sage: bij.constant_blocks() + {} + sage: bij.constant_blocks(optimal=True) + {{[], [1], [1, 2], [2, 1]}} + + In this other example we look at permutations with length 2 and 3:: + + sage: N = 4 + sage: A = B = [permutation for n in range(2, N) for permutation in Permutations(n)] + sage: tau = lambda p: p[0] if len(p) else 0 + sage: add_n = lambda p1: Permutation(p1 + [1 + len(p1)]) + sage: add_1 = lambda p1: Permutation([1] + [1 + i for i in p1]) + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_intertwining_relations((1, add_n, lambda x: x + 1), (1, add_1, lambda x: x + 1)) + sage: bij.set_statistics((len, len)) + + sage: bij.constant_blocks() + {} + sage: bij.constant_blocks(optimal=True) + {{[1, 3, 2], [2, 1, 3]}} + + Indeed, ``[1,3,2]`` and ``[2,1,3]`` have the same value in + all solutions, but different values are possible:: + + sage: pi1 = Permutation([1,3,2]); pi2 = Permutation([2,1,3]); + sage: set([(solution[pi1], solution[pi2]) for solution in bij.solutions_iterator()]) + {(2, 2), (3, 3)} + + Another example involving the cycle type of permutations:: + + sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] + sage: bij = Bijectionist(A, B, lambda x: x.cycle_type()) + + Let us require that each permutation has the same value as its inverse:: + + sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition + sage: P = orbit_decomposition([permutation for n in range(4) for permutation in Permutations(n)], Permutation.inverse) + sage: bij.set_constant_blocks(P) + sage: bij.constant_blocks() + {{[2, 3, 1], [3, 1, 2]}} + + sage: concat = lambda p1, p2: Permutation(p1 + [i + len(p1) for i in p2]) + sage: union = lambda p1, p2: Partition(sorted(list(p1) + list(p2), reverse=True)) + sage: bij.set_intertwining_relations((2, concat, union)) + + In this case we do not discover constant blocks by looking at the intertwining_relations only:: + + sage: next(bij.solutions_iterator()) + ... + sage: bij.constant_blocks() + {{[2, 3, 1], [3, 1, 2]}} + + sage: bij.constant_blocks(optimal=True) + {{[1, 3, 2], [2, 1, 3], [3, 2, 1]}, {[2, 3, 1], [3, 1, 2]}} + + TESTS:: + + sage: N = 4 + sage: A = B = [permutation for n in range(N + 1) for permutation in Permutations(n)] + sage: alpha1 = lambda p: len(p.weak_excedences()) + sage: alpha2 = lambda p: len(p.fixed_points()) + sage: beta1 = lambda p: len(p.descents(final_descent=True)) if p else 0 + sage: beta2 = lambda p: len([e for (e, f) in zip(p, p[1:] + [0]) if e == f + 1]) + sage: tau = Permutation.longest_increasing_subsequence_length + sage: def rotate_permutation(p): + ....: cycle = Permutation(tuple(range(1, len(p) + 1))) + ....: return Permutation([cycle.inverse()(p(cycle(i))) for i in range(1, len(p) + 1)]) + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_statistics((alpha1, beta1), (alpha2, beta2)) + sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition + sage: bij.set_constant_blocks(orbit_decomposition(A, rotate_permutation)) + sage: for p in bij.constant_blocks(): print(list(p)) + [[2, 1, 3, 4], [1, 2, 4, 3], [1, 3, 2, 4], [4, 2, 3, 1]] + [[3, 2, 1], [1, 3, 2], [2, 1, 3]] + [[2, 4, 3, 1], [3, 2, 4, 1], [2, 3, 1, 4], [1, 3, 4, 2]] + [[1, 4, 2, 3], [3, 1, 2, 4], [4, 2, 1, 3], [4, 1, 3, 2]] + [[1, 4, 3, 2], [3, 2, 1, 4]] + [[2, 1, 4, 3], [4, 3, 2, 1]] + [[2, 4, 1, 3], [3, 4, 2, 1], [4, 3, 1, 2], [3, 1, 4, 2]] + + sage: for p in bij.constant_blocks(optimal=True): sorted(p, key=len) + [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4]] + [[1, 3, 2], + [2, 1, 3], + [3, 2, 1], + [2, 3, 4, 1], + [1, 3, 4, 2], + [2, 1, 3, 4], + [1, 3, 2, 4], + [2, 3, 1, 4], + [1, 2, 4, 3], + [3, 2, 4, 1], + [2, 1, 4, 3], + [2, 4, 3, 1], + [4, 2, 3, 1], + [4, 3, 2, 1], + [1, 4, 3, 2], + [3, 2, 1, 4]] + [[1, 4, 2, 3], + [4, 2, 1, 3], + [2, 4, 1, 3], + [4, 3, 1, 2], + [4, 1, 3, 2], + [3, 4, 2, 1], + [3, 1, 2, 4], + [3, 1, 4, 2]] + + The permutation `[2, 1]` is in none of these blocks:: + + sage: bij.set_constant_blocks(orbit_decomposition(A, rotate_permutation)) + sage: all(s[Permutation([2, 1])] == s[Permutation([1])] for s in bij.solutions_iterator()) + False + + sage: all(s[Permutation([2, 1])] == s[Permutation([1, 3, 2])] for s in bij.solutions_iterator()) + False + + sage: all(s[Permutation([2, 1])] == s[Permutation([1, 4, 2, 3])] for s in bij.solutions_iterator()) + False + + + + sage: A = B = ["a", "b", "c", "d", "e", "f"] + sage: tau = {"a": 1, "b": 1, "c": 3, "d": 4, "e": 5, "f": 6}.get + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_distributions((["a", "b"], [1, 1]), (["c", "d", "e"], [3, 4, 5])) + sage: bij.constant_blocks() + {} + sage: bij.constant_blocks(optimal=True) + {{'a', 'b'}} + + sage: A = B = ["a", "b", "c", "d", "e", "f"] + sage: tau = {"a": 1, "b": 1, "c": 5, "d": 4, "e": 4, "f": 6}.get + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_distributions((["a", "b"], [1, 1]), (["d", "e"], [4, 4])) + sage: bij.constant_blocks(optimal=True) + {{'a', 'b'}, {'d', 'e'}} + + sage: A = B = ["a", "b", "c", "d"] + sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2}.get + sage: bij = Bijectionist(A, B, tau) + sage: bij.constant_blocks(optimal=True) + {} + sage: bij.set_constant_blocks([["a", "b"]]) + sage: bij.constant_blocks() + {{'a', 'b'}} + sage: bij.constant_blocks(optimal=True) + {{'a', 'b'}, {'c', 'd'}} + + """ + bmilp = self._generate_and_solve_initial_bmilp() # may throw Exception + + # generate blockwise preimage to determine which blocks have the same image + solution = self._solution_by_blocks(bmilp) + multiple_preimages = {(value,): preimages + for value, preimages in _invert_dict(solution).items() + if len(preimages) > 1} + + # check for each pair of blocks if a solution with different values on these block exists + # if yes, use the new solution to update the multiple_preimages dictionary, restart the check + # if no, the two blocks can be joined + + # _P has to be copied to not mess with the solution-process + # since we do not want to regenerate the bmilp in each step, so blocks + # have to stay consistent during the whole process + tmp_P = deepcopy(self._P) + updated_preimages = True + while updated_preimages: + updated_preimages = False + for values in copy(multiple_preimages): # copy to be able to modify dict + if updated_preimages: + break + for i, j in itertools.combinations(copy(multiple_preimages[values]), r=2): # copy to be able to modify list + bmilp_veto = deepcopy(bmilp) # adding constraints to a simple copy adds them to the original instance, too + try: + # veto the two blocks having the same value + for z in self._possible_block_values[i]: + if z in self._possible_block_values[j]: # intersection + bmilp_veto.milp.add_constraint(bmilp_veto._x[i, z] + bmilp_veto._x[j, z] <= 1) + bmilp_veto.milp.solve() + + # solution exists, update dictionary + solution = self._solution_by_blocks(bmilp_veto) + updated_multiple_preimages = {} + for values in multiple_preimages: + for p in multiple_preimages[values]: + solution_tuple = (*values, solution[p]) # tuple so actual solutions were equal in lookup + if solution_tuple not in updated_multiple_preimages: + updated_multiple_preimages[solution_tuple] = [] + updated_multiple_preimages[solution_tuple].append(p) + updated_preimages = True + multiple_preimages = updated_multiple_preimages + break + except MIPSolverException: + # no solution exists, join blocks + tmp_P.union(i, j) + if i in multiple_preimages[values] and j in multiple_preimages[values]: # only one of the joined blocks should remain in the list + multiple_preimages[values].remove(j) + if len(multiple_preimages[values]) == 1: + del multiple_preimages[values] + break + + self.set_constant_blocks(tmp_P) + + def possible_values(self, p=None, optimal=False): + r""" + Return for each block the values of `s` compatible with the + imposed restrictions. + + TODO: should this method update and return ``self._possible_block_values``? + + INPUT: + + - ``p`` (optional, default: ``None``) -- a block of `P`, or + an element of a block of `P`, or a list of these + + - ``optimal`` (optional, default: ``False``) -- whether or + not to compute the minimal possible set of statistic values, + throws a MIPSolverException if no solution is found. + + .. NOTE:: + + computing the minimal possible set of statistic values + may be computationally expensive. + + TESTS:: + + sage: A = B = ["a", "b", "c", "d"] + sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2}.get + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_constant_blocks([["a", "b"]]) + + Test if all formats are really possible:: + + sage: bij.possible_values(p="a") + {'a': {1, 2}, 'b': {1, 2}} + sage: bij.possible_values(p=["a", "b"]) + {'a': {1, 2}, 'b': {1, 2}} + sage: bij.possible_values(p=[["a", "b"]]) + {'a': {1, 2}, 'b': {1, 2}} + sage: bij.possible_values(p=[["a", "b"], ["c"]]) + {'a': {1, 2}, 'b': {1, 2}, 'c': {1, 2}} + + Test optimal:: + + sage: bij.possible_values(p=["a", "c"], optimal=True) + {'a': {1, 2}, 'b': {1, 2}, 'c': {1, 2}} + + Verify by listing all solutions:: + + sage: sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))) + [{'a': 1, 'b': 1, 'c': 2, 'd': 2}, {'a': 2, 'b': 2, 'c': 1, 'd': 1}] + + Test if MIPSolverException is thrown:: + + sage: A = B = list('ab') + sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2) + sage: bij.set_constant_blocks([['a', 'b']]) + sage: bij.possible_values(p="a") + {'a': {0, 1}, 'b': {0, 1}} + sage: bij.possible_values(p="a", optimal=True) + Traceback (most recent call last): + ... + sage.numerical.mip.MIPSolverException: ... + + Another example:: + + sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] + sage: tau = Permutation.longest_increasing_subsequence_length + sage: bij = Bijectionist(A, B, tau) + sage: alpha = lambda p: p(1) if len(p) > 0 else 0 + sage: beta = lambda p: p(1) if len(p) > 0 else 0 + sage: bij.set_statistics((alpha, beta), (len, len)) + sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): + ....: print(sol) + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2} + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1} + sage: bij.possible_values(p=[Permutation([1]), Permutation([1, 2, 3]), Permutation([3, 1, 2])], optimal=True) + {[1]: {1}, [1, 2, 3]: {2, 3}, [3, 1, 2]: {1, 2}} + + Another example:: + + sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)] + sage: tau = lambda D: D.number_of_touch_points() + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength())) + sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): + ....: print(solution) + {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 1, [1, 1, 0, 0]: 2} + {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1} + sage: bij.possible_values(p=[DyckWord([]), DyckWord([1, 0]), DyckWord([1, 0, 1, 0]), DyckWord([1, 1, 0, 0])], optimal=True) + {[]: {0}, [1, 0]: {1}, [1, 0, 1, 0]: {1, 2}, [1, 1, 0, 0]: {1, 2}} + + .. TODO: + + test der zeigt, dass die Lösung für alle Blöcke nicht + langsamer ist als mit solutions_iterator + + """ + # convert input to set of block representatives + blocks = set() + if p in self._A: + blocks.add(self._P.find(p)) + elif type(p) is list: + for p1 in p: + if p1 in self._A: + blocks.add(self._P.find(p1)) + elif type(p1) is list: + for p2 in p1: + blocks.add(self._P.find(p2)) + + if optimal: + # function adding a solution to dict of solutions + def add_solution(solutions, solution): + for p, value in solution.items(): + if p not in solutions: + solutions[p] = set() + solutions[p].add(value) + + # generate initial solution, solution dict and add solution + bmilp = self._generate_and_solve_initial_bmilp() + solution = self._solution(bmilp) + solutions = {} + add_solution(solutions, solution) + + # iterate through blocks and generate all values + for p in blocks: + veto_bmilp = deepcopy(bmilp) # adding constraints to a simple copy adds them to the original instance, too + for value in solutions[p]: + veto_bmilp.milp.add_constraint(veto_bmilp._x[p, value] == 0) + while True: + try: + veto_bmilp.milp.solve() + # problem has a solution, so new value was found + solution = self._solution(veto_bmilp) + add_solution(solutions, solution) + # veto new value and try again + veto_bmilp.milp.add_constraint(veto_bmilp._x[p, solution[p]] == 0) + except MIPSolverException: + # no solution, so all possible values have been found + break + + # TODO: update possible block values if wanted + + # create dictionary to return + possible_values = {} + for p in blocks: + for a in self._P.root_to_elements_dict()[p]: # TODO: is this the format we want to return in or possible_values[block]? + if optimal: + possible_values[a] = solutions[p] + else: + possible_values[a] = self._possible_block_values[p] + + return possible_values + + def minimal_subdistributions_iterator(self, tA=None): + r""" + Return all minimal subsets `\tilde A` of `A` containing `tA` + together with submultisets `\tilde Z` with `s(\tilde A) = + \tilde Z` as multisets. + + TODO: should this method interact with ``self._elements_distributions``? + + INPUT: + + - ``tA`` (optional, default: ``None``) -- a subset of `A` TODO: add this + + If ``tA`` is not ``None``, return an iterator of the + subdistributions containing ``tA``. + + TESTS:: + + sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)] + sage: bij = Bijectionist(A, B, len) + sage: bij.set_statistics((len, len)) + sage: for sol in bij.solutions_iterator(): + ....: print(sol) + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 2} + sage: sorted(bij.minimal_subdistributions_iterator()) + [([[]], [0]), ([[1]], [1]), ([[1, 2]], [2]), ([[2, 1]], [2])] + + Another example:: + + sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)] + sage: tau = lambda D: D.number_of_touch_points() + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength())) + sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): + ....: print(solution) + {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 1, [1, 1, 0, 0]: 2} + {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1} + sage: for subdistribution in bij.minimal_subdistributions_iterator(): + ....: print(subdistribution) + ([[]], [0]) + ([[1, 0]], [1]) + ([[1, 0, 1, 0], [1, 1, 0, 0]], [1, 2]) + + An example with two elements of the same block in a subdistribution:: + + sage: A = B = ["a", "b", "c", "d", "e"] + sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2, "e": 3}.get + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_constant_blocks([["a", "b"]]) + sage: bij.set_value_restrictions(("a", [1, 2])) + sage: bij.constant_blocks(optimal=True) + {{'a', 'b'}} + sage: list(bij.minimal_subdistributions_iterator()) + [(['a', 'b', 'c', 'd', 'e'], [1, 1, 2, 2, 3])] + """ + # see + # https://mathoverflow.net/questions/406751/find-a-subdistribution/406975 + # and + # https://gitlab.com/mantepse/bijection-tools/-/issues/29 + + minimal_subdistribution = MixedIntegerLinearProgram(maximization=False, solver=self._solver) + D = minimal_subdistribution.new_variable(binary=True) # the subset of elements + V = minimal_subdistribution.new_variable(integer=True) # the subdistribution + minimal_subdistribution.set_objective(sum(D[a] for a in self._A)) + minimal_subdistribution.add_constraint(sum(D[a] for a in self._A) >= 1) + + try: + bmilp = self._generate_and_solve_initial_bmilp() + except MIPSolverException: + return + s = self._solution(bmilp) + while True: + for v in self._Z: + minimal_subdistribution.add_constraint(sum(D[a] for a in self._A if s[a] == v) == V[v]) + try: + minimal_subdistribution.solve() + except MIPSolverException: + return + d = minimal_subdistribution.get_values(D) # a dict from A to {0, 1} + new_s = self._find_counter_example(bmilp, s, d) + if new_s is None: + values = self._sorter["Z"](s[a] for a in self._A if d[a]) + yield ([a for a in self._A if d[a]], values) + + # get all variables with value 1 + active_vars = [D[a] for a in self._A + if minimal_subdistribution.get_values(D[a])] + + # add constraint that not all of these can be 1, thus vetoing + # the current solution + minimal_subdistribution.add_constraint(sum(active_vars) <= len(active_vars) - 1, + name="veto") + # TODO: can we ignore that in the next step the same constraint is added again? + else: + s = new_s + + def _find_counter_example(self, bmilp, s0, d): + r""" + Return a solution `s` such that ``d`` is not a subdistribution of + `s0`. + + TODO: better name + + INPUT: + + - ``bmilp``, the mixed linear integer program + + - ``s0``, a solution + + - ``d``, a subset of `A`, in the form of a dict from `A` to `\{0, 1\}` + """ + for v in self._Z: + v_in_d_count = sum(d[a] for a in self._A if s0[a] == v) + if not v_in_d_count: + continue + + veto_bmilp = deepcopy(bmilp) # adding constraints to a simple copy adds them to the original instance, too + # try to find a solution which has a different + # subdistribution on d than s0 + v_in_d = sum(d[a] * veto_bmilp._x[self._P.find(a), v] + for a in self._A + if v in self._possible_block_values[self._P.find(a)]) + + # it is sufficient to require that v occurs less often as + # a value among {a | d[a] == 1} than it does in + # v_in_d_count, because, if the distributions are + # different, one such v must exist + veto_bmilp.milp.add_constraint(v_in_d <= v_in_d_count - 1) + try: + veto_bmilp.milp.solve() + return self._solution(veto_bmilp) + except MIPSolverException: + pass + return + + def minimal_subdistributions_blocks_iterator(self, p=None): + r"""Return all representatives of minimal subsets `\tilde P` + of `P` containing `p` together with submultisets `\tilde Z` + with `s(\tilde P) = \tilde Z` as multisets. + + .. WARNING:: + + If there are several solutions with the same support + (i.e., the sets of block representatives are the same), + only one of these will be found, even if the + distributions are different, see the doctest below. To + find all solutions, use + :meth:`minimal_subdistributions_iterator`, which is, + however, computationally more expensive. + + TODO: should this method interact with ``self._elements_distributions``? + + INPUT: + + - ``p`` (optional, default: ``None``) -- a subset of `P` TODO: add this + + If ``p`` is not ``None``, return an iterator of the + subdistributions containing ``p``. + + EXAMPLES:: + + sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)] + sage: bij = Bijectionist(A, B, len) + sage: bij.set_statistics((len, len)) + sage: for sol in bij.solutions_iterator(): + ....: print(sol) + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 2} + sage: sorted(bij.minimal_subdistributions_blocks_iterator()) + [([[]], [0]), ([[1]], [1]), ([[1, 2]], [2]), ([[2, 1]], [2])] + + Another example:: + + sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)] + sage: tau = lambda D: D.number_of_touch_points() + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength())) + sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): + ....: print(solution) + {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 1, [1, 1, 0, 0]: 2} + {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1} + sage: for subdistribution in bij.minimal_subdistributions_blocks_iterator(): + ....: print(subdistribution) + ([[]], [0]) + ([[1, 0]], [1]) + ([[1, 0, 1, 0], [1, 1, 0, 0]], [1, 2]) + + An example with two elements of the same block in a subdistribution:: + + sage: A = B = ["a", "b", "c", "d", "e"] + sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2, "e": 3}.get + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_constant_blocks([["a", "b"]]) + sage: bij.set_value_restrictions(("a", [1, 2])) + sage: bij.constant_blocks(optimal=True) + {{'a', 'b'}} + sage: list(bij.minimal_subdistributions_blocks_iterator()) + [(['a', 'a', 'c', 'd', 'e'], [1, 1, 2, 2, 3])] + + An example with overlapping minimal subdistributions:: + + sage: A = B = ["a", "b", "c", "d", "e"] + sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2, "e": 3}.get + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_distributions((["a", "b"], [1, 2]), (["a", "c", "d"], [1, 2, 3])) + sage: sorted(bij.solutions_iterator(), key=lambda d: tuple(sorted(d.items()))) + [{'a': 1, 'b': 2, 'c': 2, 'd': 3, 'e': 1}, + {'a': 1, 'b': 2, 'c': 3, 'd': 2, 'e': 1}, + {'a': 2, 'b': 1, 'c': 1, 'd': 3, 'e': 2}, + {'a': 2, 'b': 1, 'c': 3, 'd': 1, 'e': 2}] + sage: bij.constant_blocks(optimal=True) + {{'a', 'e'}} + sage: list(bij.minimal_subdistributions_blocks_iterator()) + [(['a', 'b'], [1, 2]), (['a', 'c', 'd'], [1, 2, 3])] + + Fedor Petrov's example from https://mathoverflow.net/q/424187:: + + sage: A = B = ["a"+str(i) for i in range(1, 9)] + ["b"+str(i) for i in range(3, 9)] + ["d"] + sage: tau = {b: 0 if i < 10 else 1 for i, b in enumerate(B)}.get + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_constant_blocks([["a"+str(i), "b"+str(i)] for i in range(1, 9) if "b"+str(i) in A]) + sage: d = [0]*8+[1]*4 + sage: bij.set_distributions((A[:8] + A[8+2:-1], d), (A[:8] + A[8:-3], d)) + sage: sorted([s[a] for a in A] for s in bij.solutions_iterator()) + [[0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1], + [0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0], + [0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0], + [0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0], + [1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0], + [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0], + [1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0], + [1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0], + [1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1], + [1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]] + + sage: sorted(bij.minimal_subdistributions_blocks_iterator()) + [(['a1', 'a2', 'a3', 'a4', 'a5', 'a5', 'a6', 'a6', 'a7', 'a7', 'a8', 'a8'], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1]), + (['a3', 'a4', 'd'], [0, 0, 1]), + (['a7', 'a8', 'd'], [0, 0, 1])] + + The following solution is not found, because it happens to + have the same support as the other:: + + sage: D = set(A).difference(['b7', 'b8', 'd']) + sage: sorted(a.replace("b", "a") for a in D) + ['a1', 'a2', 'a3', 'a3', 'a4', 'a4', 'a5', 'a5', 'a6', 'a6', 'a7', 'a8'] + sage: set(tuple(sorted(s[a] for a in D)) for s in bij.solutions_iterator()) + {(0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1)} + + But it is, by design, included here:: + + sage: sorted(D) in [d for d, _ in bij.minimal_subdistributions_iterator()] + True + + """ + # see + # https://mathoverflow.net/questions/406751/find-a-subdistribution/406975 + # and + # https://gitlab.com/mantepse/bijection-tools/-/issues/29 + # see https://mathoverflow.net/q/424187 for Fedor Petrov's example + + minimal_subdistribution = MixedIntegerLinearProgram(maximization=False, solver=self._solver) + D = minimal_subdistribution.new_variable(integer=True, nonnegative=True) # the submultiset of elements + X = minimal_subdistribution.new_variable(binary=True) # the support of D + V = minimal_subdistribution.new_variable(integer=True, nonnegative=True) # the subdistribution + P = _disjoint_set_roots(self._P) + minimal_subdistribution.set_objective(sum(D[p] for p in P)) + minimal_subdistribution.add_constraint(sum(D[p] for p in P) >= 1) + for p in P: + minimal_subdistribution.add_constraint(D[p] <= len(self._P.root_to_elements_dict()[p])) + minimal_subdistribution.add_constraint(X[p]*len(self._P.root_to_elements_dict()[p]) >= D[p] >= X[p]) + + def add_counter_example_constraint(s): + for v in self._Z: + minimal_subdistribution.add_constraint(sum(D[p] for p in P + if s[p] == v) == V[v]) + + try: + bmilp = self._generate_and_solve_initial_bmilp() + except MIPSolverException: + return + s = self._solution_by_blocks(bmilp) + add_counter_example_constraint(s) + while True: + try: + minimal_subdistribution.solve() + except MIPSolverException: + return + d = minimal_subdistribution.get_values(D) # a dict from P to multiplicities + new_s = self._find_counter_example2(bmilp, P, s, d) + if new_s is None: + yield ([p for p in P for _ in range(ZZ(d[p]))], + self._sorter["Z"](s[p] + for p in P + for _ in range(ZZ(d[p])))) + + support = [X[p] for p in P if d[p]] + # add constraint that the support is different + minimal_subdistribution.add_constraint(sum(support) <= len(support) - 1, + name="veto") + else: + s = new_s + add_counter_example_constraint(s) + + def _find_counter_example2(self, bmilp, P, s0, d): + r""" + Return a solution `s` such that ``d`` is not a subdistribution of + `s0`. + + TODO: better name + + INPUT: + + - ``bmilp``, the mixed linear integer program + + - ``P``, the representatives of the blocks + + - ``s0``, a solution + + - ``d``, a subset of `A`, in the form of a dict from `A` to `\{0, 1\}` + """ + for v in self._Z: + v_in_d_count = sum(d[p] for p in P if s0[p] == v) + if not v_in_d_count: + continue + + veto_bmilp = deepcopy(bmilp) # adding constraints to a simple copy adds them to the original instance, too + # try to find a solution which has a different + # subdistribution on d than s0 + v_in_d = sum(d[p] * veto_bmilp._x[p, v] + for p in P + if v in self._possible_block_values[p]) + + # it is sufficient to require that v occurs less often as + # a value among {a | d[a] == 1} than it does in + # v_in_d_count, because, if the distributions are + # different, one such v must exist + veto_bmilp.milp.add_constraint(v_in_d <= v_in_d_count - 1) + try: + veto_bmilp.milp.solve() + return self._solution_by_blocks(veto_bmilp) + except MIPSolverException: + pass + return + + def _preprocess_intertwining_relations(self): + r""" + TODO: (medium) untangle side effect and return value if possible + + Make `self._P` be the finest set partition coarser than `self._P` + such that composing elements preserves blocks. + + Suppose that `p_1`, `p_2` are blocks of `P`, and `a_1, a'_1 + \in p_1` and `a_2, a'_2\in p_2`. Then, + + .. MATH: + + s(\pi(a_1, a_2)) + = \rho(s(a_1), s(a_2)) + = \rho(s(a'_1), s(a'_2)) + = s(\pi(a'_1, a'_2)). + + Therefore, `\pi(a_1, a_2)` and `\pi(a'_1, a'_2)` are in the + same block. + + In other words, `s(\pi(a_1,\dots,a_k))` only depends on the + blocks of `a_1,\dots,a_k`. + + TESTS:: + + TODO: create one test with one and one test with two + intertwining_relations + + """ + images = {} # A^k -> A, a_1,...,a_k to pi(a_1,...,a_k), for all pi + origins_by_elements = [] # (pi/rho, pi(a_1,...,a_k), a_1,...,a_k) + for composition_index, pi_rho in enumerate(self._pi_rho): + for a_tuple in itertools.product(*([self._A]*pi_rho.numargs)): + if pi_rho.domain is not None and not pi_rho.domain(*a_tuple): + continue + a = pi_rho.pi(*a_tuple) + if a in self._A: + if a in images: + # this happens if there are several pi's of the same arity + images[a_tuple].add(a) # TODO: (low) wouldn't self._P.find(a) be more efficient here? + else: + images[a_tuple] = set((a,)) # TODO: (low) wouldn't self._P.find(a) be more efficient here? + origins_by_elements.append((composition_index, a, a_tuple)) + + # merge blocks + something_changed = True + while something_changed: + something_changed = False + # collect (preimage, image) pairs by (representatives) of + # the blocks of the elements of the preimage + updated_images = {} # (p_1,...,p_k) to {a_1,....} + for a_tuple, image_set in images.items(): + representatives = tuple(self._P.find(a) for a in a_tuple) + if representatives in updated_images: + updated_images[representatives].update(image_set) + else: + updated_images[representatives] = image_set + + # merge blocks + for a_tuple, image_set in updated_images.items(): + image = image_set.pop() + while image_set: + self._P.union(image, image_set.pop()) + something_changed = True + # we keep a representative + image_set.add(image) + + images = updated_images + + origins = set() + for composition_index, image, preimage in origins_by_elements: + origins.add((composition_index, + self._P.find(image), + tuple(self._P.find(a) for a in preimage))) + return origins + + def solutions_iterator(self): + r""" + An iterator over all solutions of the problem. + + OUTPUT: An iterator over all possible mappings `s: A\to Z` + + ALGORITHM: + + We solve an integer linear program with a binary variable + `x_{p, z}` for each partition block `p\in P` and each + statistic value `z\in Z`: + + - `x_{p, z} = 1` if and only if `s(a) = z` for all `a\in p`. + + Then we add the constraint `\sum_{x\in V} x<|V|`, where `V` + is the set containing all `x` with `x = 1`, that is, those + indicator variables representing the current solution. + Therefore, a solution of this new program must be different + from all those previously obtained. + + INTEGER LINEAR PROGRAM: + + * Let `m_w(p)`, for a block `p` of `P`, be the multiplicity + of the value `w` in `W` under `\alpha`, that is, the number + of elements `a \in p` with `\alpha(a)=w`. + + * Let `n_w(z)` be the number of elements `b \in B` with + `\beta(b)=w` and `\tau(b)=z` for `w \in W`, `z \in Z`. + + * Let `k` be the arity of a pair `(\pi, \rho)` in an + intertwining relation. + + and the following constraints: + + * because every block is assigned precisely one value, for + all `p\in P`, + + .. MATH:: + + \sum_z x_{p, z} = 1. + + * because the statistics `s` and `\tau` and also `\alpha` and + `\beta` are equidistributed, for all `w\in W` and `z\in Z`, + + .. MATH:: + + \sum_p m_w(p) x_{p, z} = n_w(z). + + * for each intertwining relation `s(\pi(a_1,\dots, a_k)) = + \rho(s(a_1),\dots, s(a_r))`, and for all `k`-combinations + of blocks `p_i\in P` such that there exist `(a_1,\dots, + a_k)\in p_1\times\dots\times p_k` with `\pi(a_1,\dots, + a_k)\in W` and `z = \rho(z_1,\dots, z_k)`, + + .. MATH:: + + x_{p, z} \geq 1-k + \sum_{i=1}^k x_{p_i, z_i}. + + * for each distribution restriction, i.e. a set of elements + `e` and a distribution of values given by integers `d_z` + representing the multiplicity of each `z \in Z`, and `r_p = + |p \cap e|` indicating the relative size of block `p` in + the set of elements of the distribution, + + .. MATH:: + + \sum_p r_p x_{p, z} = d_z. + + EXAMPLES:: + + sage: A = B = list('abc') + sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2, solver="GLPK") + sage: next(bij.solutions_iterator()) + {'a': 0, 'b': 1, 'c': 0} + + sage: list(bij.solutions_iterator()) + [{'a': 0, 'b': 1, 'c': 0}, + {'a': 1, 'b': 0, 'c': 0}, + {'a': 0, 'b': 0, 'c': 1}] + + sage: N = 4 + sage: A = B = [permutation for n in range(N) for permutation in Permutations(n)] + + Let `\tau` be the number of non-left-to-right-maxima of a + permutation:: + + sage: def tau(pi): + ....: pi = list(pi) + ....: i = count = 0 + ....: for j in range(len(pi)): + ....: if pi[j] > i: + ....: i = pi[j] + ....: else: + ....: count += 1 + ....: return count + + We look for a statistic which is constant on conjugacy classes:: + + sage: P = [list(a) for n in range(N) for a in Permutations(n).conjugacy_classes()] + + sage: bij = Bijectionist(A, B, tau, solver="GLPK") + sage: bij.set_statistics((len, len)) + sage: bij.set_constant_blocks(P) + sage: for solution in bij.solutions_iterator(): + ....: print(solution) + {[]: 0, [1]: 0, [1, 2]: 1, [2, 1]: 0, [1, 2, 3]: 0, [1, 3, 2]: 1, [2, 1, 3]: 1, [3, 2, 1]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2} + {[]: 0, [1]: 0, [1, 2]: 0, [2, 1]: 1, [1, 2, 3]: 0, [1, 3, 2]: 1, [2, 1, 3]: 1, [3, 2, 1]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2} + + Setting the verbosity prints the MILP which is solved:: + + sage: set_verbose(2) + sage: _ = list(bij.solutions_iterator()) + Constraints are: + block []: 1 <= x_0 <= 1 + block [1]: 1 <= x_1 <= 1 + block [1, 2]: 1 <= x_2 + x_3 <= 1 + block [2, 1]: 1 <= x_4 + x_5 <= 1 + block [1, 2, 3]: 1 <= x_6 + x_7 + x_8 <= 1 + block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1 + block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1 + statistics: 1 <= x_0 <= 1 + statistics: 1 <= x_1 <= 1 + statistics: 1 <= x_2 + x_4 <= 1 + statistics: 1 <= x_3 + x_5 <= 1 + statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1 + statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3 + statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2 + Variables are: + x_0: s([]) = 0 + x_1: s([1]) = 0 + x_2: s([1, 2]) = 0 + x_3: s([1, 2]) = 1 + x_4: s([2, 1]) = 0 + x_5: s([2, 1]) = 1 + x_6: s([1, 2, 3]) = 0 + x_7: s([1, 2, 3]) = 1 + x_8: s([1, 2, 3]) = 2 + x_9: s([1, 3, 2]) = s([2, 1, 3]) = s([3, 2, 1]) = 0 + x_10: s([1, 3, 2]) = s([2, 1, 3]) = s([3, 2, 1]) = 1 + x_11: s([1, 3, 2]) = s([2, 1, 3]) = s([3, 2, 1]) = 2 + x_12: s([2, 3, 1]) = s([3, 1, 2]) = 0 + x_13: s([2, 3, 1]) = s([3, 1, 2]) = 1 + x_14: s([2, 3, 1]) = s([3, 1, 2]) = 2 + after vetoing + Constraints are: + block []: 1 <= x_0 <= 1 + block [1]: 1 <= x_1 <= 1 + block [1, 2]: 1 <= x_2 + x_3 <= 1 + block [2, 1]: 1 <= x_4 + x_5 <= 1 + block [1, 2, 3]: 1 <= x_6 + x_7 + x_8 <= 1 + block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1 + block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1 + statistics: 1 <= x_0 <= 1 + statistics: 1 <= x_1 <= 1 + statistics: 1 <= x_2 + x_4 <= 1 + statistics: 1 <= x_3 + x_5 <= 1 + statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1 + statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3 + statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2 + veto: x_0 + x_1 + x_3 + x_4 + x_6 + x_10 + x_14 <= 6 + after vetoing + Constraints are: + block []: 1 <= x_0 <= 1 + block [1]: 1 <= x_1 <= 1 + block [1, 2]: 1 <= x_2 + x_3 <= 1 + block [2, 1]: 1 <= x_4 + x_5 <= 1 + block [1, 2, 3]: 1 <= x_6 + x_7 + x_8 <= 1 + block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1 + block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1 + statistics: 1 <= x_0 <= 1 + statistics: 1 <= x_1 <= 1 + statistics: 1 <= x_2 + x_4 <= 1 + statistics: 1 <= x_3 + x_5 <= 1 + statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1 + statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3 + statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2 + veto: x_0 + x_1 + x_3 + x_4 + x_6 + x_10 + x_14 <= 6 + veto: x_0 + x_1 + x_2 + x_5 + x_6 + x_10 + x_14 <= 6 + + sage: set_verbose(0) + + TESTS: + + An unfeasible problem:: + + sage: A = ["a", "b", "c", "d"]; B = [1, 2, 3, 4] + sage: bij = Bijectionist(A, B) + sage: bij.set_value_restrictions(("a", [1, 2]), ("b", [1, 2]), ("c", [1, 3]), ("d", [2, 3])) + sage: list(bij.solutions_iterator()) + [] + + """ + try: + bmilp = self._generate_and_solve_initial_bmilp() + except MIPSolverException: + return + while True: + yield self._solution(bmilp) + bmilp.veto_current_solution() + if get_verbose() >= 2: + print("after vetoing") + self._show_bmilp(bmilp, variables=False) + try: + bmilp.milp.solve() + except MIPSolverException: + return + + def _solution(self, bmilp): + """ + Return the bmilp solution as a dictionary from `A` to + `Z`. + + """ + map = {} # A -> Z, a +-> s(a) + for p, block in self._P.root_to_elements_dict().items(): + for z in self._possible_block_values[p]: + if bmilp.milp.get_values(bmilp._x[p, z]) == 1: + for a in block: + map[a] = z + break + return map + + def _solution_by_blocks(self, bmilp): + """ + Return the bmilp solution as a dictionary from block + representatives of `P` to `Z`. + + """ + map = {} # P -> Z, a +-> s(a) + for p in _disjoint_set_roots(self._P): + for z in self._possible_block_values[p]: + if bmilp.milp.get_values(bmilp._x[p, z]) == 1: + map[p] = z + break + return map + + def _show_bmilp(self, bmilp, variables=True): + """ + Print the constraints and variables of the current MILP + together with some explanations. + + """ + print("Constraints are:") + b = bmilp.milp.get_backend() + varid_name = {} + for i in range(b.ncols()): + s = b.col_name(i) + default_name = str(bmilp.milp.linear_functions_parent()({i: 1})) + if s and s != default_name: + varid_name[i] = s + else: + varid_name[i] = default_name + for i, (lb, (indices, values), ub) in enumerate(bmilp.milp.constraints()): + if b.row_name(i): + print(" "+b.row_name(i)+":", end=" ") + if lb is not None: + print(str(ZZ(lb))+" <=", end=" ") + first = True + for j, c in sorted(zip(indices, values)): + c = ZZ(c) + if c == 0: + continue + print((("+ " if (not first and c > 0) else "") + + ("" if c == 1 else + ("- " if c == -1 else + (str(c) + " " if first and c < 0 else + ("- " + str(abs(c)) + " " if c < 0 else str(c) + " ")))) + + varid_name[j]), end=" ") + first = False + # Upper bound + print("<= "+str(ZZ(ub)) if ub is not None else "") + + if variables: + print("Variables are:") + for (p, z), v in bmilp._x.items(): + print(f" {v}: " + "".join([f"s({a}) = " + for a in self._P.root_to_elements_dict()[p]]) + f"{z}") + + def _generate_and_solve_initial_bmilp(self): + r""" + Generate a _BijectionistMILP, add all relevant constraints and call MILP.solve(). + """ + preimage_blocks = self._preprocess_intertwining_relations() + self._compute_possible_block_values() + + bmilp = _BijectionistMILP(self) + n = bmilp.milp.number_of_variables() + bmilp.add_alpha_beta_constraints() + bmilp.add_distribution_constraints() + bmilp.add_interwining_relation_constraints(preimage_blocks) + if get_verbose() >= 2: + self._show_bmilp(bmilp) + assert n == bmilp.milp.number_of_variables(), "The number of variables increased." + bmilp.milp.solve() + return bmilp + + +class _BijectionistMILP(SageObject): + r""" + Wrapper class for the MixedIntegerLinearProgram (MILP). This class is used to manage the MILP, + add constraints, solve the problem and check for uniqueness of solution values. + """ + def __init__(self, bijectionist: Bijectionist): + # TODO: it would be cleaner not to pass the full bijectionist + # instance, but only those attributes we actually use: + # _possible_block_values + # _elements_distributions + # _W, _Z, _A, _B, _P, _alpha, _beta, _tau, _pi_rho + self.milp = MixedIntegerLinearProgram(solver=bijectionist._solver) + self.milp.set_objective(None) + self._x = self.milp.new_variable(binary=True) # indexed by P x Z + + self._bijectionist = bijectionist + + for p in _disjoint_set_roots(bijectionist._P): + name = f"block {p}" + self.milp.add_constraint(sum(self._x[p, z] + for z in bijectionist._possible_block_values[p]) == 1, + name=name[:50]) + + def add_alpha_beta_constraints(self): + r""" + Add constraints enforcing that `(alpha, s)` is equidistributed + with `(beta, tau)` and `S` is the intertwining bijection. + + We do this by adding + + .. MATH:: + + \sum_{a\in A, z\in Z} x_{p(a), z} s^z t^{\alpha(a)} + = \sum_{b\in B} s^{\tau(b)} t(\beta(b)) + + as a matrix equation. + + """ + W = self._bijectionist._W + Z = self._bijectionist._Z + AZ_matrix = [[ZZ(0)]*len(W) for _ in range(len(Z))] + B_matrix = [[ZZ(0)]*len(W) for _ in range(len(Z))] + + W_dict = {w: i for i, w in enumerate(W)} + Z_dict = {z: i for i, z in enumerate(Z)} + + for a in self._bijectionist._A: + p = self._bijectionist._P.find(a) + for z in self._bijectionist._possible_block_values[p]: + w_index = W_dict[self._bijectionist._alpha(a)] + z_index = Z_dict[z] + AZ_matrix[z_index][w_index] += self._x[p, z] + + for b in self._bijectionist._B: + w_index = W_dict[self._bijectionist._beta(b)] + z_index = Z_dict[self._bijectionist._tau[b]] + B_matrix[z_index][w_index] += 1 + + # TODO: (low) I am not sure that this is the best way to + # filter out empty conditions + for w in range(len(W)): + for z in range(len(Z)): + c = AZ_matrix[z][w] - B_matrix[z][w] + if c.is_zero(): + continue + if c in ZZ: + raise MIPSolverException + self.milp.add_constraint(c == 0, name="statistics") + + def add_distribution_constraints(self): + r""" + Add constraints so the distributions given by + :meth:`~Bijectionist.set_distributions` are fulfilled. + + To accomplish this we add + + .. MATH:: + + \sum_{a\in elements} x_{p(a), z}t^z = \sum_{z\in values} t^z, + + where `p(a)` is the block containing `a`, for each given + distribution as a vector equation. + + """ + Z = self._bijectionist._Z + Z_dict = {z: i for i, z in enumerate(Z)} + for elements, values in self._bijectionist._elements_distributions: + elements_sum = [ZZ(0)]*len(Z_dict) + values_sum = [ZZ(0)]*len(Z_dict) + for a in elements: + p = self._bijectionist._P.find(a) + for z in self._bijectionist._possible_block_values[p]: + elements_sum[Z_dict[z]] += self._x[p, z] + for z in values: + values_sum[Z_dict[z]] += 1 + + # TODO: (low) I am not sure that this is the best way to + # filter out empty conditions + for element, value in zip(elements_sum, values_sum): + c = element - value + if c.is_zero(): + continue + if c in ZZ: + raise MIPSolverException + self.milp.add_constraint(c == 0, name=f"d: {element} == {value}") + + def add_interwining_relation_constraints(self, origins): + r""" + Add constraints corresponding to the given intertwining + relations. + + This adds the constraints imposed by + :meth:`~Bijectionist.set_intertwining_relations`. + + .. MATH:: + + s(\pi(a_1,\dots, a_k)) = \rho(s(a_1),\dots, s(a_k))` + + for each pair `(\pi, \rho)`. The relation implies + immediately that `s(\pi(a_1,\dots, a_k))` only depends on the + blocks of `a_1,\dots, a_k`. + + The MILP formulation is as follows. Let `a_1,\dots,a_k \in + A` and let `a = \pi(a_1,\dots,a_k)`. Let `z_1,\dots,z_k \in + Z` and let `z = \rho(z_1,\dots,z_k)`. Suppose that `a_i\in + p_i` for all `i` and that `a\in p`. + + We then want to model the implication + + .. MATH:: + + x_{p_1, z_1} = 1,\dots, x_{p_k, z_k} = 1 \Rightarrow x_{p, z} = 1. + + We achieve this by requiring + + .. MATH:: + + x_{p, z}\geq 1 - k + \sum_{i=1}^k x_{p_i, z_i}. + + Not that `z` must be a possible value of `p` and each `z_i` + must be a possible value of `p_i`. + + INPUT: + + - origins, a list of triples `((\pi/\rho, p, + (p_1,\dots,p_k))`, where `p` is the block of + `\rho(s(a_1),\dots, s(a_k))`, for any `a_i\in p_i`. + + TODO: TESTS + + """ + for composition_index, image_block, preimage_blocks in origins: + pi_rho = self._bijectionist._pi_rho[composition_index] + # iterate over all possible value combinations of the origin blocks + for z_tuple in itertools.product(*[self._bijectionist._possible_block_values[p] + for p in preimage_blocks]): + rhs = 1 - pi_rho.numargs + sum(self._x[p_i, z_i] + for p_i, z_i in zip(preimage_blocks, z_tuple)) + z = pi_rho.rho(*z_tuple) + if z in self._bijectionist._possible_block_values[image_block]: + c = self._x[image_block, z] - rhs + if c.is_zero(): + continue + self.milp.add_constraint(c >= 0, + name=f"pi/rho({composition_index})") + else: + self.milp.add_constraint(rhs <= 0, + name=f"pi/rho({composition_index})") + + def veto_current_solution(self): + r""" + Add a constraint vetoing the current solution. + + This adds a constraint such that the next call to + :meth:`MixedIntegerLinearProgram.solve()` must return a + solution different from the current one. + + We require that the MILP currently has a solution. + + .. WARNING:: + + The underlying MILP will be modified! + + ALGORITHM: + + We add the constraint `\sum_{x\in V} x < |V|`` where `V` is + the set of variables `x_{p, z}` with value 1, that is, the + set of variables indicating the current solution. + + """ + # get all variables with value 1 + active_vars = [self._x[p, z] + for p in _disjoint_set_roots(self._bijectionist._P) + for z in self._bijectionist._possible_block_values[p] + if self.milp.get_values(self._x[p, z])] + + # add constraint that not all of these can be 1, thus vetoing + # the current solution + self.milp.add_constraint(sum(active_vars) <= len(active_vars) - 1, + name="veto") + + +def _invert_dict(d): + """ + Return the dictionary whose keys are the values of the input and + whose values are the lists of preimages. + """ + preimages = {} + for k, v in d.items(): + preimages[v] = preimages.get(v, []) + [k] + return preimages + + +def _disjoint_set_roots(d): + """ + Return the representatives of the blocks of the disjoint set. + """ + return d.root_to_elements_dict().keys() + + +""" +TESTS:: + + sage: As = Bs = [[], + ....: [(1,i,j) for i in [-1,0,1] for j in [-1,1]], + ....: [(2,i,j) for i in [-1,0,1] for j in [-1,1]], + ....: [(3,i,j) for i in [-2,-1,0,1,2] for j in [-1,1]]] + + # adding [(2,-2,-1), (2,2,-1), (2,-2,1), (2,2,1)] makes it take (seemingly) forever + + sage: c1 = lambda a, b: (a[0]+b[0], a[1]*b[1], a[2]*b[2]) + sage: c2 = lambda a: (a[0], -a[1], a[2]) + + sage: bij = Bijectionist(sum(As, []), sum(Bs, [])) + sage: bij.set_statistics((lambda x: x[0], lambda x: x[0])) + sage: bij.set_intertwining_relations((2, c1, c1), (1, c2, c2)) + sage: l = list(bij.solutions_iterator()); len(l) + 64 + +A brute force check would be difficult:: + + sage: prod([factorial(len(A)) for A in As]) + 1881169920000 + +Let us try a smaller example:: + + sage: As = Bs = [[], + ....: [(1,i,j) for i in [-1,0,1] for j in [-1,1]], + ....: [(2,i,j) for i in [-1,1] for j in [-1,1]], + ....: [(3,i,j) for i in [-1,1] for j in [-1,1]]] + + sage: bij = Bijectionist(sum(As, []), sum(Bs, [])) + sage: bij.set_statistics((lambda x: x[0], lambda x: x[0])) + sage: bij.set_intertwining_relations((2, c1, c1), (1, c2, c2)) + sage: l1 = list(bij.solutions_iterator()); len(l1) + 16 + sage: prod([factorial(len(A)) for A in As]) + 414720 + + sage: pis = cartesian_product([Permutations(len(A)) for A in As]) + sage: it = ({a: Bs[n][pi[n][i]-1] for n, A in enumerate(As) for i, a in enumerate(A)} for pi in pis) + sage: A = sum(As, []) + sage: respects_c1 = lambda s: all(c1(a1, a2) not in A or s[c1(a1, a2)] == c1(s[a1], s[a2]) for a1 in A for a2 in A) + sage: respects_c2 = lambda s: all(c2(a1) not in A or s[c2(a1)] == c2(s[a1]) for a1 in A) + sage: l2 = [s for s in it if respects_c1(s) and respects_c2(s)] + sage: sorted(l1, key=lambda s: tuple(s.items())) == l2 + True + +""" + + +""" +Our benchmark example:: + + sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition + sage: alpha1 = lambda p: len(p.weak_excedences()) + sage: alpha2 = lambda p: len(p.fixed_points()) + sage: beta1 = lambda p: len(p.descents(final_descent=True)) if p else 0 + sage: beta2 = lambda p: len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1]) + sage: gamma = Permutation.longest_increasing_subsequence_length + sage: def rotate_permutation(p): + ....: cycle = Permutation(tuple(range(1, len(p)+1))) + ....: return Permutation([cycle.inverse()(p(cycle(i))) for i in range(1, len(p)+1)]) + + sage: N=5; As = [list(Permutations(n)) for n in range(N+1)]; A = B = sum(As, []); bij = Bijectionist(A, B, gamma); bij.set_statistics((len, len), (alpha1, beta1), (alpha2, beta2)); bij.set_constant_blocks(sum([orbit_decomposition(A, rotate_permutation) for A in As], [])) + + sage: P = bij.constant_blocks(optimal=True) + sage: P = [sorted(p, key=lambda p: (len(p), p)) for p in P] + sage: P = sorted(P, key=lambda p: (len(next(iter(p))), len(p))) + sage: for p in P: + ....: print(p) + [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5]] + [[2, 1], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], ... + [[3, 1, 2], [1, 4, 2, 3], [2, 4, 1, 3], [3, 1, 2, 4], [3, 1, 4, 2], ... + [[4, 1, 2, 3], [1, 5, 2, 3, 4], [4, 1, 2, 3, 5], [4, 5, 1, 2, 3], [5, 1, 2, 4, 3], ... + [[1, 3, 2, 5, 4], [2, 1, 3, 5, 4], [2, 1, 4, 3, 5], [5, 2, 4, 3, 1], [5, 3, 2, 4, 1]] + [[1, 3, 5, 2, 4], [2, 4, 1, 3, 5], [3, 5, 2, 4, 1], [4, 1, 3, 5, 2], [5, 2, 4, 1, 3]] + ... + + sage: for d in sorted(bij.minimal_subdistributions_blocks_iterator(), key=lambda d: (len(d[0]), d[0])): + ....: print(d) + ([[]], [0]) + ([[1]], [1]) + ([[2, 1]], [2]) + ([[3, 1, 2]], [3]) + ([[4, 1, 2, 3]], [4]) + ([[5, 1, 2, 3, 4]], [5]) + ([[2, 1, 4, 5, 3], [2, 3, 5, 1, 4], [2, 4, 1, 5, 3], [2, 4, 5, 1, 3]], [2, 3, 3, 3]) + ([[2, 1, 5, 3, 4], [2, 5, 1, 3, 4], [3, 1, 5, 2, 4], [3, 5, 1, 2, 4]], [3, 3, 4, 4]) + ([[1, 3, 2, 5, 4], [1, 3, 5, 2, 4], [1, 4, 2, 5, 3], [1, 4, 5, 2, 3], [1, 4, 5, 3, 2], [1, 5, 4, 2, 3], [1, 5, 4, 3, 2]], [2, 2, 3, 3, 3, 3, 3]) + + sage: l = list(bij.solutions_iterator()); len(l) # long time + 504 + + sage: for a, d in bij.minimal_subdistributions_iterator(): # long time + ....: print(sorted(a), sorted(d)) +""" From 6149eb25ff2301cd9ac7f41f8c2c6a6da367dccc Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Tue, 18 Oct 2022 15:23:48 +0200 Subject: [PATCH 02/51] make the linter happier --- src/sage/combinat/bijectionist.py | 76 ++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 27 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 452a013b470..12ce11ba64f 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -383,18 +383,13 @@ from copy import copy, deepcopy from sage.misc.verbose import get_verbose -# TODO: (low) für sagemath sollten wir Zeilen möglichst auf 79 -# Zeichen beschränken, das ist zwar nicht streng, wird aber lieber -# gesehen. - # TODO: (low) we frequently need variable names for subsets of A, B, # Z. In LaTeX, we mostly call them \tilde A, \tilde Z, etc. now. It # would be good to have a standard name in code, too. -# TODO: (medium) wann immer möglich, sollten die Tests in einer -# Methode nur diese eine Methode testen. Wir haben in fast allen -# Methoden "system tests", das ist unpraktisch, wenn man größere -# Änderungen durchführt. +# TODO: (medium) whenever possible, doctests of a method should only +# test this method. Currently we have very many system tests, which +# is inconvenient when modifying the design substantially. class Bijectionist(SageObject): @@ -1000,7 +995,13 @@ def set_value_restrictions(self, *a_values): However, an error occurs if the set of possible values is empty. In this example, the image of `\tau` under any - legal bijection is disjoint to the specified values. :: TODO: we now have to call _compute_possible_block_values() for the error message. Is this intended behaviour? + legal bijection is disjoint to the specified values. + + .. TODO:: + + we now have to call + :meth:`_compute_possible_block_values` for the error + message. Is this intended behaviour? sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] sage: tau = Permutation.longest_increasing_subsequence_length @@ -1044,7 +1045,8 @@ def _compute_possible_block_values(self): """ self._possible_block_values = {} # P -> Power(Z) for p, block in self._P.root_to_elements_dict().items(): - self._possible_block_values[p] = set.intersection(*[self._restrictions_possible_values[a] for a in block], *[self._statistics_possible_values[a] for a in block]) + self._possible_block_values[p] = set.intersection(*[self._restrictions_possible_values[a] for a in block], + *[self._statistics_possible_values[a] for a in block]) if not self._possible_block_values[p]: if len(block) == 1: raise ValueError(f"No possible values found for singleton block {block}") @@ -1531,11 +1533,13 @@ def _forced_constant_blocks(self): self.set_constant_blocks(tmp_P) def possible_values(self, p=None, optimal=False): - r""" - Return for each block the values of `s` compatible with the + r"""Return for each block the values of `s` compatible with the imposed restrictions. - TODO: should this method update and return ``self._possible_block_values``? + .. TODO:: + + should this method update and return + ``self._possible_block_values``? INPUT: @@ -1621,10 +1625,10 @@ def possible_values(self, p=None, optimal=False): sage: bij.possible_values(p=[DyckWord([]), DyckWord([1, 0]), DyckWord([1, 0, 1, 0]), DyckWord([1, 1, 0, 0])], optimal=True) {[]: {0}, [1, 0]: {1}, [1, 0, 1, 0]: {1, 2}, [1, 1, 0, 0]: {1, 2}} - .. TODO: + .. TODO:: - test der zeigt, dass die Lösung für alle Blöcke nicht - langsamer ist als mit solutions_iterator + test to show that the solution for all blocks is not more + expensive than using :meth:`solutions_iterator` """ # convert input to set of block representatives @@ -1689,7 +1693,9 @@ def minimal_subdistributions_iterator(self, tA=None): together with submultisets `\tilde Z` with `s(\tilde A) = \tilde Z` as multisets. - TODO: should this method interact with ``self._elements_distributions``? + .. TODO:: + + should this method interact with ``self._elements_distributions``? INPUT: @@ -1832,15 +1838,21 @@ def minimal_subdistributions_blocks_iterator(self, p=None): :meth:`minimal_subdistributions_iterator`, which is, however, computationally more expensive. - TODO: should this method interact with ``self._elements_distributions``? + .. TODO:: + + should this method interact with ``self._elements_distributions``? INPUT: - - ``p`` (optional, default: ``None``) -- a subset of `P` TODO: add this + - ``p`` (optional, default: ``None``) -- a subset of `P` If ``p`` is not ``None``, return an iterator of the subdistributions containing ``p``. + .. TODO:: + + the optional argument is not yet supported + EXAMPLES:: sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)] @@ -1992,7 +2004,10 @@ def _find_counter_example2(self, bmilp, P, s0, d): Return a solution `s` such that ``d`` is not a subdistribution of `s0`. - TODO: better name + .. TODO:: + + find a better name - possibly not relevant if we + implement the cache of solutions INPUT: @@ -2003,6 +2018,7 @@ def _find_counter_example2(self, bmilp, P, s0, d): - ``s0``, a solution - ``d``, a subset of `A`, in the form of a dict from `A` to `\{0, 1\}` + """ for v in self._Z: v_in_d_count = sum(d[p] for p in P if s0[p] == v) @@ -2030,7 +2046,10 @@ def _find_counter_example2(self, bmilp, P, s0, d): def _preprocess_intertwining_relations(self): r""" - TODO: (medium) untangle side effect and return value if possible + + .. TODO:: + + (medium) untangle side effect and return value if possible Make `self._P` be the finest set partition coarser than `self._P` such that composing elements preserves blocks. @@ -2051,10 +2070,10 @@ def _preprocess_intertwining_relations(self): In other words, `s(\pi(a_1,\dots,a_k))` only depends on the blocks of `a_1,\dots,a_k`. - TESTS:: + .. TODO:: - TODO: create one test with one and one test with two - intertwining_relations + create one test with one and one test with two + intertwining_relations """ images = {} # A^k -> A, a_1,...,a_k to pi(a_1,...,a_k), for all pi @@ -2549,8 +2568,6 @@ def add_interwining_relation_constraints(self, origins): (p_1,\dots,p_k))`, where `p` is the block of `\rho(s(a_1),\dots, s(a_k))`, for any `a_i\in p_i`. - TODO: TESTS - """ for composition_index, image_block, preimage_blocks in origins: pi_rho = self._bijectionist._pi_rho[composition_index] @@ -2685,7 +2702,12 @@ def _disjoint_set_roots(d): ....: cycle = Permutation(tuple(range(1, len(p)+1))) ....: return Permutation([cycle.inverse()(p(cycle(i))) for i in range(1, len(p)+1)]) - sage: N=5; As = [list(Permutations(n)) for n in range(N+1)]; A = B = sum(As, []); bij = Bijectionist(A, B, gamma); bij.set_statistics((len, len), (alpha1, beta1), (alpha2, beta2)); bij.set_constant_blocks(sum([orbit_decomposition(A, rotate_permutation) for A in As], [])) + sage: N=5 + sage: As = [list(Permutations(n)) for n in range(N+1)] + sage: A = B = sum(As, []) + sage: bij = Bijectionist(A, B, gamma) + sage: bij.set_statistics((len, len), (alpha1, beta1), (alpha2, beta2)) + sage: bij.set_constant_blocks(sum([orbit_decomposition(A, rotate_permutation) for A in As], [])) sage: P = bij.constant_blocks(optimal=True) sage: P = [sorted(p, key=lambda p: (len(p), p)) for p in P] From 9d4bfb97b1667fc7fc7e58527a9d8beae9e1f621 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Tue, 18 Oct 2022 18:10:06 +0200 Subject: [PATCH 03/51] remove useless assignment --- src/sage/combinat/bijectionist.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 12ce11ba64f..ac41442e414 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -719,9 +719,6 @@ def set_statistics(self, *alpha_beta): {[]: 2, [1]: 1, [1, 2]: 0, [2, 1]: 0} """ - # reset values - self._statistics_possible_values = {a: set(self._Z) for a in self._A} - self._n_statistics = len(alpha_beta) # TODO: (low) do we really want to recompute statistics every time? self._alpha = lambda p: tuple(arg[0](p) for arg in alpha_beta) @@ -1034,7 +1031,8 @@ def set_value_restrictions(self, *a_values): """ # reset values - self._restrictions_possible_values = {a: set(self._Z) for a in self._A} + set_Z = set(self._Z) + self._restrictions_possible_values = {a: set_Z.copy() for a in self._A} for a, values in a_values: assert a in self._A, f"Element {a} was not found in A" self._restrictions_possible_values[a].intersection_update(values) From 6ee34e1d761af4b8ec23c721a12a0d123318661a Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Wed, 19 Oct 2022 00:36:54 +0200 Subject: [PATCH 04/51] make initialisation more efficient --- src/sage/combinat/bijectionist.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index ac41442e414..95637fcae32 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -483,6 +483,16 @@ class Bijectionist(SageObject): """ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], pi_rho=tuple(), elements_distributions=tuple(), a_values=tuple(), solver=None, key=None): + """ + Initialize the bijectionist. + + TESTS: + + Check that large input sets are handled well:: + + sage: A = B = list(range(7000)) + sage: bij = Bijectionist(A, B) + """ # glossary of standard letters: # A, B, Z, W ... finite sets # ???? tilde_A, tilde_Z, ..., subsets? @@ -746,9 +756,14 @@ def set_statistics(self, *alpha_beta): self._W = list(self._statistics_fibers) # the possible values of s(a) are tau(beta^{-1}(alpha(a))) - self._statistics_possible_values = {a: set(self._tau[b] - for b in self._statistics_fibers[self._alpha(a)][1]) - for a in self._A} + tau_beta_inverse = {} + self._statistics_possible_values = {} + for a in self._A: + alpha_a = self._alpha(a) + if alpha_a not in tau_beta_inverse: + tau_beta_inverse[alpha_a] = set(self._tau[b] + for b in self._statistics_fibers[alpha_a][1]) + self._statistics_possible_values[a] = tau_beta_inverse[alpha_a] def statistics_fibers(self): r""" @@ -1032,10 +1047,10 @@ def set_value_restrictions(self, *a_values): """ # reset values set_Z = set(self._Z) - self._restrictions_possible_values = {a: set_Z.copy() for a in self._A} + self._restrictions_possible_values = {a: set_Z for a in self._A} for a, values in a_values: assert a in self._A, f"Element {a} was not found in A" - self._restrictions_possible_values[a].intersection_update(values) + self._restrictions_possible_values[a] = self._restrictions_possible_values[a].intersection(values) def _compute_possible_block_values(self): r""" From 1a14bcfcbd8202fb81481a0017c1df0974547797 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Wed, 19 Oct 2022 00:50:14 +0200 Subject: [PATCH 05/51] consistently name variable --- src/sage/combinat/bijectionist.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 95637fcae32..10c076858dc 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -759,11 +759,11 @@ def set_statistics(self, *alpha_beta): tau_beta_inverse = {} self._statistics_possible_values = {} for a in self._A: - alpha_a = self._alpha(a) - if alpha_a not in tau_beta_inverse: - tau_beta_inverse[alpha_a] = set(self._tau[b] - for b in self._statistics_fibers[alpha_a][1]) - self._statistics_possible_values[a] = tau_beta_inverse[alpha_a] + v = self._alpha(a) + if v not in tau_beta_inverse: + tau_beta_inverse[v] = set(self._tau[b] + for b in self._statistics_fibers[v][1]) + self._statistics_possible_values[a] = tau_beta_inverse[v] def statistics_fibers(self): r""" From 0901ec7a2dd6de58de2307489e844dd2978d79bb Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Wed, 19 Oct 2022 17:02:14 +0200 Subject: [PATCH 06/51] slightly improve documentation --- src/sage/combinat/bijectionist.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 10c076858dc..0124994c022 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -393,7 +393,9 @@ class Bijectionist(SageObject): - r"""Solver class for bijection-statistic problems. + r""" + A toolbox to list all possible bijections between two finite sets + under various constraints. INPUT: @@ -517,6 +519,7 @@ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], pi_rho=tuple(), ele self._tau = {b: b for b in self._B} else: self._tau = {b: tau(b) for b in self._B} + # we store Z as a list to keep an order self._Z = set(self._tau.values()) if key is not None and "Z" in key: self._sorter["Z"] = lambda x: sorted(x, key=key["Z"]) @@ -526,8 +529,8 @@ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], pi_rho=tuple(), ele self._Z = sorted(self._Z) self._sorter["Z"] = lambda x: sorted(x) except TypeError: - self._sorter["Z"] = lambda x: list(x) self._Z = list(self._Z) + self._sorter["Z"] = lambda x: list(x) # set optional inputs self.set_statistics(*alpha_beta) @@ -1045,7 +1048,10 @@ def set_value_restrictions(self, *a_values): AssertionError: Element (1, 2) was not found in A """ - # reset values + # it might be much cheaper to construct the sets as subsets + # of _statistics_possible_values - however, we do not want to + # insist that set_value_restrictions is called after + # set_statistics set_Z = set(self._Z) self._restrictions_possible_values = {a: set_Z for a in self._A} for a, values in a_values: @@ -1055,6 +1061,15 @@ def set_value_restrictions(self, *a_values): def _compute_possible_block_values(self): r""" Update the dictionary of possible values of each block. + + This has to be called whenever `self._P` was modified. + + .. TODO:: + + If `self._Z` is large, this is very memory expensive. In + this case it would be good if equal values of the dictionary + `self._possible_block_values` would share memory. + """ self._possible_block_values = {} # P -> Power(Z) for p, block in self._P.root_to_elements_dict().items(): @@ -1546,7 +1561,8 @@ def _forced_constant_blocks(self): self.set_constant_blocks(tmp_P) def possible_values(self, p=None, optimal=False): - r"""Return for each block the values of `s` compatible with the + r""" + Return for each block the values of `s` compatible with the imposed restrictions. .. TODO:: @@ -1837,7 +1853,8 @@ def _find_counter_example(self, bmilp, s0, d): return def minimal_subdistributions_blocks_iterator(self, p=None): - r"""Return all representatives of minimal subsets `\tilde P` + r""" + Return all representatives of minimal subsets `\tilde P` of `P` containing `p` together with submultisets `\tilde Z` with `s(\tilde P) = \tilde Z` as multisets. @@ -2416,7 +2433,9 @@ def _show_bmilp(self, bmilp, variables=True): def _generate_and_solve_initial_bmilp(self): r""" - Generate a _BijectionistMILP, add all relevant constraints and call MILP.solve(). + Generate a ``_BijectionistMILP``, add all relevant constraints + and call ``MILP.solve()``. + """ preimage_blocks = self._preprocess_intertwining_relations() self._compute_possible_block_values() From c39d653071d1d00b73fca4bf961bab8d4a7ba360 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Wed, 2 Nov 2022 10:55:37 +0100 Subject: [PATCH 07/51] non-copying intersection, to save memory when there are almost no restrictions on the values of a block --- src/sage/combinat/bijectionist.py | 51 +++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 0124994c022..4b4b52089d6 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -13,6 +13,12 @@ # subdistributions with "smallest" (e.g., in the sorting order # defined above) elements first? +# Priorities: + +# 1) for an all-knowing user, code should be as fast as possible +# 2) code should be easy to understand +# 3) anticipate that a user computes things in a bad order + r""" A bijectionist's toolkit @@ -492,7 +498,7 @@ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], pi_rho=tuple(), ele Check that large input sets are handled well:: - sage: A = B = list(range(7000)) + sage: A = B = list(range(20000)) sage: bij = Bijectionist(A, B) """ # glossary of standard letters: @@ -1062,19 +1068,13 @@ def _compute_possible_block_values(self): r""" Update the dictionary of possible values of each block. - This has to be called whenever `self._P` was modified. - - .. TODO:: - - If `self._Z` is large, this is very memory expensive. In - this case it would be good if equal values of the dictionary - `self._possible_block_values` would share memory. - + This has to be called whenever ``self._P`` was modified. """ self._possible_block_values = {} # P -> Power(Z) for p, block in self._P.root_to_elements_dict().items(): - self._possible_block_values[p] = set.intersection(*[self._restrictions_possible_values[a] for a in block], - *[self._statistics_possible_values[a] for a in block]) + sets = ([self._restrictions_possible_values[a] for a in block] + + [self._statistics_possible_values[a] for a in block]) + self._possible_block_values[p] = _non_copying_intersection(sets) if not self._possible_block_values[p]: if len(block) == 1: raise ValueError(f"No possible values found for singleton block {block}") @@ -2435,7 +2435,6 @@ def _generate_and_solve_initial_bmilp(self): r""" Generate a ``_BijectionistMILP``, add all relevant constraints and call ``MILP.solve()``. - """ preimage_blocks = self._preprocess_intertwining_relations() self._compute_possible_block_values() @@ -2670,6 +2669,34 @@ def _disjoint_set_roots(d): return d.root_to_elements_dict().keys() +def _non_copying_intersection(sets): + """ + Return the intersection of the sets. + + If the intersection is equal to one of the sets, return this + set. + + EXAMPLES:: + + sage: from sage.combinat.bijectionist import _non_copying_intersection + sage: A = set(range(7000)); B = set(range(8000)); + sage: _non_copying_intersection([A, B]) is A + True + + """ + sets = sorted(sets, key=len) + result = set.intersection(*sets) + n = len(result) + if n < len(sets[0]): + return result + for s in sets: + N = len(s) + if N > n: + return result + if s == result: + return s + + """ TESTS:: From 321ba43b09501549f459a0ec0e70349ade428c92 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Mon, 5 Dec 2022 09:41:46 +0100 Subject: [PATCH 08/51] start to cache solutions --- src/sage/combinat/bijectionist.py | 186 +++++++++++++++++++++++++----- 1 file changed, 154 insertions(+), 32 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 4b4b52089d6..fd2c804ede1 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -1529,16 +1529,16 @@ def _forced_constant_blocks(self): if updated_preimages: break for i, j in itertools.combinations(copy(multiple_preimages[values]), r=2): # copy to be able to modify list - bmilp_veto = deepcopy(bmilp) # adding constraints to a simple copy adds them to the original instance, too + tmp_constraints = [] try: # veto the two blocks having the same value for z in self._possible_block_values[i]: if z in self._possible_block_values[j]: # intersection - bmilp_veto.milp.add_constraint(bmilp_veto._x[i, z] + bmilp_veto._x[j, z] <= 1) - bmilp_veto.milp.solve() + tmp_constraints.append(bmilp._x[i, z] + bmilp._x[j, z] <= 1) + bmilp.solve(tmp_constraints) # solution exists, update dictionary - solution = self._solution_by_blocks(bmilp_veto) + solution = self._solution_by_blocks(bmilp) updated_multiple_preimages = {} for values in multiple_preimages: for p in multiple_preimages[values]: @@ -1688,17 +1688,17 @@ def add_solution(solutions, solution): # iterate through blocks and generate all values for p in blocks: - veto_bmilp = deepcopy(bmilp) # adding constraints to a simple copy adds them to the original instance, too + tmp_constraints = [] for value in solutions[p]: - veto_bmilp.milp.add_constraint(veto_bmilp._x[p, value] == 0) + tmp_constraints.append(bmilp._x[p, value] == 0) while True: try: - veto_bmilp.milp.solve() # problem has a solution, so new value was found - solution = self._solution(veto_bmilp) + bmilp.solve(tmp_constraints) + solution = self._solution(bmilp) add_solution(solutions, solution) # veto new value and try again - veto_bmilp.milp.add_constraint(veto_bmilp._x[p, solution[p]] == 0) + tmp_constraints.append(bmilp._x[p, solution[p]] == 0) except MIPSolverException: # no solution, so all possible values have been found break @@ -1788,6 +1788,7 @@ def minimal_subdistributions_iterator(self, tA=None): except MIPSolverException: return s = self._solution(bmilp) + tmp_constraints = [] while True: for v in self._Z: minimal_subdistribution.add_constraint(sum(D[a] for a in self._A if s[a] == v) == V[v]) @@ -1833,10 +1834,9 @@ def _find_counter_example(self, bmilp, s0, d): if not v_in_d_count: continue - veto_bmilp = deepcopy(bmilp) # adding constraints to a simple copy adds them to the original instance, too # try to find a solution which has a different # subdistribution on d than s0 - v_in_d = sum(d[a] * veto_bmilp._x[self._P.find(a), v] + v_in_d = sum(d[a] * bmilp._x[self._P.find(a), v] for a in self._A if v in self._possible_block_values[self._P.find(a)]) @@ -1844,10 +1844,10 @@ def _find_counter_example(self, bmilp, s0, d): # a value among {a | d[a] == 1} than it does in # v_in_d_count, because, if the distributions are # different, one such v must exist - veto_bmilp.milp.add_constraint(v_in_d <= v_in_d_count - 1) + tmp_constraints = [v_in_d <= v_in_d_count - 1] try: - veto_bmilp.milp.solve() - return self._solution(veto_bmilp) + bmilp.solve(tmp_constraints) + return self._solution(bmilp) except MIPSolverException: pass return @@ -2055,10 +2055,9 @@ def _find_counter_example2(self, bmilp, P, s0, d): if not v_in_d_count: continue - veto_bmilp = deepcopy(bmilp) # adding constraints to a simple copy adds them to the original instance, too # try to find a solution which has a different # subdistribution on d than s0 - v_in_d = sum(d[p] * veto_bmilp._x[p, v] + v_in_d = sum(d[p] * bmilp._x[p, v] for p in P if v in self._possible_block_values[p]) @@ -2066,10 +2065,10 @@ def _find_counter_example2(self, bmilp, P, s0, d): # a value among {a | d[a] == 1} than it does in # v_in_d_count, because, if the distributions are # different, one such v must exist - veto_bmilp.milp.add_constraint(v_in_d <= v_in_d_count - 1) + tmp_constraints = [v_in_d <= v_in_d_count - 1] try: - veto_bmilp.milp.solve() - return self._solution_by_blocks(veto_bmilp) + bmilp.solve(tmp_constraints) + return self._solution_by_blocks(bmilp) except MIPSolverException: pass return @@ -2346,17 +2345,16 @@ def solutions_iterator(self): """ try: - bmilp = self._generate_and_solve_initial_bmilp() + self.bmilp = self._generate_and_solve_initial_bmilp() except MIPSolverException: return while True: - yield self._solution(bmilp) - bmilp.veto_current_solution() + yield self._solution(self.bmilp) if get_verbose() >= 2: print("after vetoing") - self._show_bmilp(bmilp, variables=False) + self._show_bmilp(self.bmilp, variables=False) try: - bmilp.milp.solve() + self.bmilp.solve([], force_new_solution=True) except MIPSolverException: return @@ -2369,7 +2367,7 @@ def _solution(self, bmilp): map = {} # A -> Z, a +-> s(a) for p, block in self._P.root_to_elements_dict().items(): for z in self._possible_block_values[p]: - if bmilp.milp.get_values(bmilp._x[p, z]) == 1: + if bmilp.get_value(p, z) == 1: for a in block: map[a] = z break @@ -2384,7 +2382,7 @@ def _solution_by_blocks(self, bmilp): map = {} # P -> Z, a +-> s(a) for p in _disjoint_set_roots(self._P): for z in self._possible_block_values[p]: - if bmilp.milp.get_values(bmilp._x[p, z]) == 1: + if bmilp.get_value(p, z) == 1: map[p] = z break return map @@ -2447,7 +2445,7 @@ def _generate_and_solve_initial_bmilp(self): if get_verbose() >= 2: self._show_bmilp(bmilp) assert n == bmilp.milp.number_of_variables(), "The number of variables increased." - bmilp.milp.solve() + bmilp.solve([]) return bmilp @@ -2464,6 +2462,11 @@ def __init__(self, bijectionist: Bijectionist): # _W, _Z, _A, _B, _P, _alpha, _beta, _tau, _pi_rho self.milp = MixedIntegerLinearProgram(solver=bijectionist._solver) self.milp.set_objective(None) + self._n_variables = -1 + self._solution_cache = [] + self._last_solution = {} + self._index_block_value_dict = None + self._block_value_index_dict = None# TODO: may not be needed? self._x = self.milp.new_variable(binary=True) # indexed by P x Z self._bijectionist = bijectionist @@ -2474,6 +2477,79 @@ def __init__(self, bijectionist: Bijectionist): for z in bijectionist._possible_block_values[p]) == 1, name=name[:50]) + def clear_solution_cache(self): + self._n_variables = -1 + self._solution_cache = [] + self._last_solution = {} + self._index_block_value_dict = None + self._block_value_index_dict = None # TODO: may not be needed? + + def solve(self, tmp_constraints, force_new_solution=False): + if self._n_variables < 0: + self._n_variables = self.milp.number_of_variables() + self._index_block_value_dict = {} + self._block_value_index_dict = {} + for (p, z), v in self._x.items(): + variable_index = next(iter(v.dict().keys())) + self._index_block_value_dict[variable_index] = (p,z) + self._block_value_index_dict[(p,z)] = variable_index + assert self._n_variables == self.milp.number_of_variables(), "The number of variables changed." # number of variables would change with creation of constraints with new variables + + # check if previous solution exists with constraints + previous_solution_exists = False + self.last_solution = {} + if not force_new_solution: + for solution in self._solution_cache: + fulfills_constraints = True + # loop through all constraints + for constraint in tmp_constraints: + # check equations + for linear_function, value in constraint.equations(): + solution_value = self._evaluate_linear_function(linear_function.dict(), self._index_block_value_dict, solution) + if solution_value != value.dict()[-1]: + fulfills_constraints = False + break + if not fulfills_constraints: + break + # check inequalities + for linear_function, value in constraint.inequalities(): + solution_value = self._evaluate_linear_function(linear_function.dict(), self._index_block_value_dict, solution) + if solution_value > value.dict()[-1]: + fulfills_constraints = False + break + if not fulfills_constraints: + break + if fulfills_constraints: + previous_solution_exists = True + self.last_solution = solution + break + + # if no previous solution satisfies the constraints, generate a new one + if not previous_solution_exists: + try: + n_constraints = self.milp.number_of_constraints() + for constraint in tmp_constraints: + self.milp.add_constraint(constraint) + self.milp.solve() + for _ in range(self.milp.number_of_constraints()-n_constraints): + self.milp.remove_constraint(n_constraints) + except MIPSolverException as error: + for _ in range(self.milp.number_of_constraints()-n_constraints): + self.milp.remove_constraint(n_constraints) + raise error + + self.last_solution = self.milp.get_values(self._x) + self._solution_cache.append(self.last_solution) + + self.veto_current_solution() + return self.last_solution + + def _evaluate_linear_function(self, linear_function_dict, block_index_dict, values): + return float(sum(linear_function_dict[index]*values[block_index_dict[index]] for index in linear_function_dict)) + + def get_value(self, p, v): + return self.last_solution[p,v] + def add_alpha_beta_constraints(self): r""" Add constraints enforcing that `(alpha, s)` is equidistributed @@ -2700,12 +2776,62 @@ def _non_copying_intersection(sets): """ TESTS:: + ##################### + # caching base test # + ##################### + sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)] + sage: tau = lambda D: D.number_of_touch_points() + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength())) + sage: bmilp = bij._generate_and_solve_initial_bmilp() + +Print the generated solution:: + + sage: bmilp.milp.get_values(bmilp._x) + {([], 0): 1.0, + ([1, 0], 1): 1.0, + ([1, 0, 1, 0], 1): 0.0, + ([1, 0, 1, 0], 2): 1.0, + ([1, 1, 0, 0], 1): 1.0, + ([1, 1, 0, 0], 2): 0.0} + +Generating a new solution that also maps `1010` to `2` fails: + + sage: from sage.combinat.bijectionist import _disjoint_set_roots + sage: permutation1010root = list(_disjoint_set_roots(bij._P))[2] + sage: permutation1010root + [1, 0, 1, 0] + sage: bmilp.solve([bmilp._x[permutation1010root, 1] <= 0.5], force_new_solution=True) + Traceback (most recent call last): + ... + MIPSolverException: GLPK: Problem has no feasible solution + +However, searching for a cached solution succeeds, for inequalities and equalities:: + + sage: bmilp.solve([bmilp._x[permutation1010root, 1] <= 0.5]) + {([], 0): 1.0, + ([1, 0], 1): 1.0, + ([1, 0, 1, 0], 1): 0.0, + ([1, 0, 1, 0], 2): 1.0, + ([1, 1, 0, 0], 1): 1.0, + ([1, 1, 0, 0], 2): 0.0} + + sage: bmilp.solve([bmilp._x[permutation1010root, 1] == 0]) + {([], 0): 1.0, + ([1, 0], 1): 1.0, + ([1, 0, 1, 0], 1): 0.0, + ([1, 0, 1, 0], 2): 1.0, + ([1, 1, 0, 0], 1): 1.0, + ([1, 1, 0, 0], 2): 0.0} + + sage: As = Bs = [[], ....: [(1,i,j) for i in [-1,0,1] for j in [-1,1]], ....: [(2,i,j) for i in [-1,0,1] for j in [-1,1]], ....: [(3,i,j) for i in [-2,-1,0,1,2] for j in [-1,1]]] - # adding [(2,-2,-1), (2,2,-1), (2,-2,1), (2,2,1)] makes it take (seemingly) forever +Note that adding ``[(2,-2,-1), (2,2,-1), (2,-2,1), (2,2,1)]`` makes +it take (seemingly) forever.:: sage: c1 = lambda a, b: (a[0]+b[0], a[1]*b[1], a[2]*b[2]) sage: c2 = lambda a: (a[0], -a[1], a[2]) @@ -2745,10 +2871,6 @@ def _non_copying_intersection(sets): sage: sorted(l1, key=lambda s: tuple(s.items())) == l2 True -""" - - -""" Our benchmark example:: sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition From 306395cf92435f6abc1656c7677cbb68ef7b02b2 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Wed, 21 Dec 2022 16:09:45 +0100 Subject: [PATCH 09/51] finish implementation of cache --- src/sage/combinat/bijectionist.py | 372 +++++++++++++++++++----------- 1 file changed, 239 insertions(+), 133 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index fd2c804ede1..75e904df59e 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -1,24 +1,4 @@ # -*- coding: utf-8 -*- -# pylint: disable=all - -# TODO: (high): -# -# check whether it makes sense to keep a list of solutions, and keep -# a global MILP up to date with this list - -# TODO: (medium): -# -# can we somehow tweak gurobi so that -# minimal_subdistributions_iterator considers the minimal -# subdistributions with "smallest" (e.g., in the sorting order -# defined above) elements first? - -# Priorities: - -# 1) for an all-knowing user, code should be as fast as possible -# 2) code should be easy to understand -# 3) anticipate that a user computes things in a bad order - r""" A bijectionist's toolkit @@ -499,7 +479,7 @@ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], pi_rho=tuple(), ele Check that large input sets are handled well:: sage: A = B = list(range(20000)) - sage: bij = Bijectionist(A, B) + sage: bij = Bijectionist(A, B) # long time """ # glossary of standard letters: # A, B, Z, W ... finite sets @@ -515,6 +495,7 @@ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], pi_rho=tuple(), ele assert len(A) == len(set(A)), "A must have distinct items" assert len(B) == len(set(B)), "B must have distinct items" + self.bmilp = None self._A = A self._B = B self._sorter = {} @@ -607,6 +588,7 @@ def set_constant_blocks(self, P): MIPSolverException: ... """ + self.bmilp = None self._P = DisjointSet(self._A) P = sorted(self._sorter["A"](p) for p in P) for p in P: @@ -738,6 +720,7 @@ def set_statistics(self, *alpha_beta): {[]: 2, [1]: 1, [1, 2]: 0, [2, 1]: 0} """ + self.bmilp = None self._n_statistics = len(alpha_beta) # TODO: (low) do we really want to recompute statistics every time? self._alpha = lambda p: tuple(arg[0](p) for arg in alpha_beta) @@ -1018,11 +1001,7 @@ def set_value_restrictions(self, *a_values): empty. In this example, the image of `\tau` under any legal bijection is disjoint to the specified values. - .. TODO:: - - we now have to call - :meth:`_compute_possible_block_values` for the error - message. Is this intended behaviour? + TESTS:: sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] sage: tau = Permutation.longest_increasing_subsequence_length @@ -1033,8 +1012,6 @@ def set_value_restrictions(self, *a_values): ... ValueError: No possible values found for singleton block [[1, 2]] - TESTS:: - sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] sage: tau = Permutation.longest_increasing_subsequence_length sage: bij = Bijectionist(A, B, tau) @@ -1058,6 +1035,7 @@ def set_value_restrictions(self, *a_values): # of _statistics_possible_values - however, we do not want to # insist that set_value_restrictions is called after # set_statistics + self.bmilp = None set_Z = set(self._Z) self._restrictions_possible_values = {a: set_Z for a in self._A} for a, values in a_values: @@ -1223,6 +1201,7 @@ def set_distributions(self, *elements_distributions): not in `A`. """ + self.bmilp = None for elements, values in elements_distributions: assert len(elements) == len(values), f"{elements} and {values} are not of the same size!" for a, z in zip(elements, values): @@ -1318,6 +1297,7 @@ def set_intertwining_relations(self, *pi_rho): [] """ + self.bmilp = None Pi_Rho = namedtuple("Pi_Rho", "numargs pi rho domain") self._pi_rho = [] @@ -1506,10 +1486,11 @@ def _forced_constant_blocks(self): {{'a', 'b'}, {'c', 'd'}} """ - bmilp = self._generate_and_solve_initial_bmilp() # may throw Exception + if self.bmilp is None: + self.bmilp = self._generate_and_solve_initial_bmilp() # may throw Exception # generate blockwise preimage to determine which blocks have the same image - solution = self._solution_by_blocks(bmilp) + solution = self._solution_by_blocks(self.bmilp) multiple_preimages = {(value,): preimages for value, preimages in _invert_dict(solution).items() if len(preimages) > 1} @@ -1534,11 +1515,11 @@ def _forced_constant_blocks(self): # veto the two blocks having the same value for z in self._possible_block_values[i]: if z in self._possible_block_values[j]: # intersection - tmp_constraints.append(bmilp._x[i, z] + bmilp._x[j, z] <= 1) - bmilp.solve(tmp_constraints) + tmp_constraints.append(self.bmilp._x[i, z] + self.bmilp._x[j, z] <= 1) + self.bmilp.solve(tmp_constraints) # solution exists, update dictionary - solution = self._solution_by_blocks(bmilp) + solution = self._solution_by_blocks(self.bmilp) updated_multiple_preimages = {} for values in multiple_preimages: for p in multiple_preimages[values]: @@ -1565,11 +1546,6 @@ def possible_values(self, p=None, optimal=False): Return for each block the values of `s` compatible with the imposed restrictions. - .. TODO:: - - should this method update and return - ``self._possible_block_values``? - INPUT: - ``p`` (optional, default: ``None``) -- a block of `P`, or @@ -1654,11 +1630,6 @@ def possible_values(self, p=None, optimal=False): sage: bij.possible_values(p=[DyckWord([]), DyckWord([1, 0]), DyckWord([1, 0, 1, 0]), DyckWord([1, 1, 0, 0])], optimal=True) {[]: {0}, [1, 0]: {1}, [1, 0, 1, 0]: {1, 2}, [1, 1, 0, 0]: {1, 2}} - .. TODO:: - - test to show that the solution for all blocks is not more - expensive than using :meth:`solutions_iterator` - """ # convert input to set of block representatives blocks = set() @@ -1681,8 +1652,9 @@ def add_solution(solutions, solution): solutions[p].add(value) # generate initial solution, solution dict and add solution - bmilp = self._generate_and_solve_initial_bmilp() - solution = self._solution(bmilp) + if self.bmilp is None: + self.bmilp = self._generate_and_solve_initial_bmilp() + solution = self._solution(self.bmilp) solutions = {} add_solution(solutions, solution) @@ -1690,25 +1662,23 @@ def add_solution(solutions, solution): for p in blocks: tmp_constraints = [] for value in solutions[p]: - tmp_constraints.append(bmilp._x[p, value] == 0) + tmp_constraints.append(self.bmilp._x[p, value] == 0) while True: try: # problem has a solution, so new value was found - bmilp.solve(tmp_constraints) - solution = self._solution(bmilp) + self.bmilp.solve(tmp_constraints) + solution = self._solution(self.bmilp) add_solution(solutions, solution) # veto new value and try again - tmp_constraints.append(bmilp._x[p, solution[p]] == 0) + tmp_constraints.append(self.bmilp._x[p, solution[p]] == 0) except MIPSolverException: # no solution, so all possible values have been found break - # TODO: update possible block values if wanted - # create dictionary to return possible_values = {} for p in blocks: - for a in self._P.root_to_elements_dict()[p]: # TODO: is this the format we want to return in or possible_values[block]? + for a in self._P.root_to_elements_dict()[p]: if optimal: possible_values[a] = solutions[p] else: @@ -1716,24 +1686,13 @@ def add_solution(solutions, solution): return possible_values - def minimal_subdistributions_iterator(self, tA=None): + def minimal_subdistributions_iterator(self): r""" - Return all minimal subsets `\tilde A` of `A` containing `tA` + Return all minimal subsets `\tilde A` of `A` together with submultisets `\tilde Z` with `s(\tilde A) = \tilde Z` as multisets. - .. TODO:: - - should this method interact with ``self._elements_distributions``? - - INPUT: - - - ``tA`` (optional, default: ``None``) -- a subset of `A` TODO: add this - - If ``tA`` is not ``None``, return an iterator of the - subdistributions containing ``tA``. - - TESTS:: + EXAMPLES:: sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)] sage: bij = Bijectionist(A, B, len) @@ -1784,11 +1743,11 @@ def minimal_subdistributions_iterator(self, tA=None): minimal_subdistribution.add_constraint(sum(D[a] for a in self._A) >= 1) try: - bmilp = self._generate_and_solve_initial_bmilp() + if self.bmilp is None: + self.bmilp = self._generate_and_solve_initial_bmilp() except MIPSolverException: return - s = self._solution(bmilp) - tmp_constraints = [] + s = self._solution(self.bmilp) while True: for v in self._Z: minimal_subdistribution.add_constraint(sum(D[a] for a in self._A if s[a] == v) == V[v]) @@ -1797,7 +1756,7 @@ def minimal_subdistributions_iterator(self, tA=None): except MIPSolverException: return d = minimal_subdistribution.get_values(D) # a dict from A to {0, 1} - new_s = self._find_counter_example(bmilp, s, d) + new_s = self._find_counter_example(self.bmilp, s, d) if new_s is None: values = self._sorter["Z"](s[a] for a in self._A if d[a]) yield ([a for a in self._A if d[a]], values) @@ -1852,10 +1811,10 @@ def _find_counter_example(self, bmilp, s0, d): pass return - def minimal_subdistributions_blocks_iterator(self, p=None): + def minimal_subdistributions_blocks_iterator(self): r""" Return all representatives of minimal subsets `\tilde P` - of `P` containing `p` together with submultisets `\tilde Z` + of `P` together with submultisets `\tilde Z` with `s(\tilde P) = \tilde Z` as multisets. .. WARNING:: @@ -1868,21 +1827,6 @@ def minimal_subdistributions_blocks_iterator(self, p=None): :meth:`minimal_subdistributions_iterator`, which is, however, computationally more expensive. - .. TODO:: - - should this method interact with ``self._elements_distributions``? - - INPUT: - - - ``p`` (optional, default: ``None``) -- a subset of `P` - - If ``p`` is not ``None``, return an iterator of the - subdistributions containing ``p``. - - .. TODO:: - - the optional argument is not yet supported - EXAMPLES:: sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)] @@ -2003,10 +1947,11 @@ def add_counter_example_constraint(s): if s[p] == v) == V[v]) try: - bmilp = self._generate_and_solve_initial_bmilp() + if self.bmilp is None: + self.bmilp = self._generate_and_solve_initial_bmilp() except MIPSolverException: return - s = self._solution_by_blocks(bmilp) + s = self._solution_by_blocks(self.bmilp) add_counter_example_constraint(s) while True: try: @@ -2014,7 +1959,7 @@ def add_counter_example_constraint(s): except MIPSolverException: return d = minimal_subdistribution.get_values(D) # a dict from P to multiplicities - new_s = self._find_counter_example2(bmilp, P, s, d) + new_s = self._find_counter_example2(self.bmilp, P, s, d) if new_s is None: yield ([p for p in P for _ in range(ZZ(d[p]))], self._sorter["Z"](s[p] @@ -2264,6 +2209,48 @@ def solutions_iterator(self): sage: set_verbose(2) sage: _ = list(bij.solutions_iterator()) + after vetoing + Constraints are: + block []: 1 <= x_0 <= 1 + block [1]: 1 <= x_1 <= 1 + block [1, 2]: 1 <= x_2 + x_3 <= 1 + block [2, 1]: 1 <= x_4 + x_5 <= 1 + block [1, 2, 3]: 1 <= x_6 + x_7 + x_8 <= 1 + block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1 + block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1 + statistics: 1 <= x_0 <= 1 + statistics: 1 <= x_1 <= 1 + statistics: 1 <= x_2 + x_4 <= 1 + statistics: 1 <= x_3 + x_5 <= 1 + statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1 + statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3 + statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2 + veto: x_0 + x_1 + x_3 + x_4 + x_6 + x_10 + x_14 <= 6 + veto: x_0 + x_1 + x_2 + x_5 + x_6 + x_10 + x_14 <= 6 + after vetoing + Constraints are: + block []: 1 <= x_0 <= 1 + block [1]: 1 <= x_1 <= 1 + block [1, 2]: 1 <= x_2 + x_3 <= 1 + block [2, 1]: 1 <= x_4 + x_5 <= 1 + block [1, 2, 3]: 1 <= x_6 + x_7 + x_8 <= 1 + block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1 + block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1 + statistics: 1 <= x_0 <= 1 + statistics: 1 <= x_1 <= 1 + statistics: 1 <= x_2 + x_4 <= 1 + statistics: 1 <= x_3 + x_5 <= 1 + statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1 + statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3 + statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2 + veto: x_0 + x_1 + x_3 + x_4 + x_6 + x_10 + x_14 <= 6 + veto: x_0 + x_1 + x_2 + x_5 + x_6 + x_10 + x_14 <= 6 + + Changing or re-setting problem parameters clears the internal cache and + prints even more information:: + + sage: bij.set_constant_blocks(P) + sage: _ = list(bij.solutions_iterator()) Constraints are: block []: 1 <= x_0 <= 1 block [1]: 1 <= x_1 <= 1 @@ -2343,18 +2330,147 @@ def solutions_iterator(self): sage: list(bij.solutions_iterator()) [] + Testing interactions between multiple instances using Fedor Petrov's example from https://mathoverflow.net/q/424187:: + + sage: A = B = ["a"+str(i) for i in range(1, 9)] + ["b"+str(i) for i in range(3, 9)] + ["d"] + sage: tau = {b: 0 if i < 10 else 1 for i, b in enumerate(B)}.get + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_constant_blocks([["a"+str(i), "b"+str(i)] for i in range(1, 9) if "b"+str(i) in A]) + sage: d = [0]*8+[1]*4 + sage: bij.set_distributions((A[:8] + A[8+2:-1], d), (A[:8] + A[8:-3], d)) + sage: iterator1 = bij.solutions_iterator() + sage: iterator2 = bij.solutions_iterator() + + Generate a solution in iterator1, iterator2 should generate the same solution and vice versa:: + sage: next(iterator1) + {'a1': 1, + 'a2': 1, + 'a3': 0, + 'a4': 0, + 'a5': 0, + 'a6': 1, + 'a7': 0, + 'a8': 0, + 'b3': 0, + 'b4': 0, + 'b5': 0, + 'b6': 1, + 'b7': 0, + 'b8': 0, + 'd': 1} + + sage: next(iterator2) + {'a1': 1, + 'a2': 1, + 'a3': 0, + 'a4': 0, + 'a5': 0, + 'a6': 1, + 'a7': 0, + 'a8': 0, + 'b3': 0, + 'b4': 0, + 'b5': 0, + 'b6': 1, + 'b7': 0, + 'b8': 0, + 'd': 1} + + sage: next(iterator2) + {'a1': 1, + 'a2': 1, + 'a3': 0, + 'a4': 0, + 'a5': 1, + 'a6': 0, + 'a7': 0, + 'a8': 0, + 'b3': 0, + 'b4': 0, + 'b5': 1, + 'b6': 0, + 'b7': 0, + 'b8': 0, + 'd': 1} + + sage: next(iterator1) + {'a1': 1, + 'a2': 1, + 'a3': 0, + 'a4': 0, + 'a5': 1, + 'a6': 0, + 'a7': 0, + 'a8': 0, + 'b3': 0, + 'b4': 0, + 'b5': 1, + 'b6': 0, + 'b7': 0, + 'b8': 0, + 'd': 1} + + Re-setting the distribution resets the cache, so a new iterator will generate the first solutions again, + but the old iterator continues:: + + sage: bij.set_distributions((A[:8] + A[8+2:-1], d), (A[:8] + A[8:-3], d)) + sage: iterator3 = bij.solutions_iterator() + + sage: next(iterator3) + {'a1': 1, + 'a2': 1, + 'a3': 0, + 'a4': 0, + 'a5': 0, + 'a6': 1, + 'a7': 0, + 'a8': 0, + 'b3': 0, + 'b4': 0, + 'b5': 0, + 'b6': 1, + 'b7': 0, + 'b8': 0, + 'd': 1} + + sage: next(iterator1) + {'a1': 0, + 'a2': 1, + 'a3': 0, + 'a4': 1, + 'a5': 0, + 'a6': 0, + 'a7': 0, + 'a8': 1, + 'b3': 0, + 'b4': 1, + 'b5': 0, + 'b6': 0, + 'b7': 0, + 'b8': 1, + 'd': 0} """ + bmilp = None + next_solution = None try: - self.bmilp = self._generate_and_solve_initial_bmilp() + if self.bmilp is None: + self.bmilp = self._generate_and_solve_initial_bmilp() + bmilp = self.bmilp + bmilp.solve([], 0) + next_solution = self._solution(bmilp) except MIPSolverException: return + + solution_index = 1 while True: - yield self._solution(self.bmilp) + yield next_solution if get_verbose() >= 2: print("after vetoing") - self._show_bmilp(self.bmilp, variables=False) + self._show_bmilp(bmilp, variables=False) try: - self.bmilp.solve([], force_new_solution=True) + bmilp.solve([], solution_index) + next_solution = self._solution(bmilp) + solution_index += 1 except MIPSolverException: return @@ -2466,7 +2582,6 @@ def __init__(self, bijectionist: Bijectionist): self._solution_cache = [] self._last_solution = {} self._index_block_value_dict = None - self._block_value_index_dict = None# TODO: may not be needed? self._x = self.milp.new_variable(binary=True) # indexed by P x Z self._bijectionist = bijectionist @@ -2477,52 +2592,42 @@ def __init__(self, bijectionist: Bijectionist): for z in bijectionist._possible_block_values[p]) == 1, name=name[:50]) - def clear_solution_cache(self): - self._n_variables = -1 - self._solution_cache = [] - self._last_solution = {} - self._index_block_value_dict = None - self._block_value_index_dict = None # TODO: may not be needed? - - def solve(self, tmp_constraints, force_new_solution=False): + def solve(self, tmp_constraints, solution_index=0): if self._n_variables < 0: self._n_variables = self.milp.number_of_variables() self._index_block_value_dict = {} - self._block_value_index_dict = {} for (p, z), v in self._x.items(): variable_index = next(iter(v.dict().keys())) - self._index_block_value_dict[variable_index] = (p,z) - self._block_value_index_dict[(p,z)] = variable_index - assert self._n_variables == self.milp.number_of_variables(), "The number of variables changed." # number of variables would change with creation of constraints with new variables + self._index_block_value_dict[variable_index] = (p, z) + # number of variables would change with creation of constraints with new variables + assert self._n_variables == self.milp.number_of_variables(), "The number of variables changed." # check if previous solution exists with constraints previous_solution_exists = False - self.last_solution = {} - if not force_new_solution: - for solution in self._solution_cache: - fulfills_constraints = True - # loop through all constraints - for constraint in tmp_constraints: - # check equations - for linear_function, value in constraint.equations(): - solution_value = self._evaluate_linear_function(linear_function.dict(), self._index_block_value_dict, solution) - if solution_value != value.dict()[-1]: - fulfills_constraints = False - break - if not fulfills_constraints: + for solution in self._solution_cache[solution_index:]: + fulfills_constraints = True + # loop through all constraints + for constraint in tmp_constraints: + # check equations + for linear_function, value in constraint.equations(): + solution_value = self._evaluate_linear_function(linear_function.dict(), self._index_block_value_dict, solution) + if solution_value != value.dict()[-1]: + fulfills_constraints = False break - # check inequalities - for linear_function, value in constraint.inequalities(): - solution_value = self._evaluate_linear_function(linear_function.dict(), self._index_block_value_dict, solution) - if solution_value > value.dict()[-1]: - fulfills_constraints = False - break - if not fulfills_constraints: + if not fulfills_constraints: + break + # check inequalities + for linear_function, value in constraint.inequalities(): + solution_value = self._evaluate_linear_function(linear_function.dict(), self._index_block_value_dict, solution) + if solution_value > value.dict()[-1]: + fulfills_constraints = False break - if fulfills_constraints: - previous_solution_exists = True - self.last_solution = solution + if not fulfills_constraints: break + if fulfills_constraints: + previous_solution_exists = True + self.last_solution = solution + break # if no previous solution satisfies the constraints, generate a new one if not previous_solution_exists: @@ -2540,15 +2645,15 @@ def solve(self, tmp_constraints, force_new_solution=False): self.last_solution = self.milp.get_values(self._x) self._solution_cache.append(self.last_solution) - self.veto_current_solution() + return self.last_solution def _evaluate_linear_function(self, linear_function_dict, block_index_dict, values): return float(sum(linear_function_dict[index]*values[block_index_dict[index]] for index in linear_function_dict)) def get_value(self, p, v): - return self.last_solution[p,v] + return self.last_solution[p, v] def add_alpha_beta_constraints(self): r""" @@ -2702,6 +2807,7 @@ def veto_current_solution(self): :meth:`MixedIntegerLinearProgram.solve()` must return a solution different from the current one. + We require that the MILP currently has a solution. .. WARNING:: @@ -2801,7 +2907,7 @@ def _non_copying_intersection(sets): sage: permutation1010root = list(_disjoint_set_roots(bij._P))[2] sage: permutation1010root [1, 0, 1, 0] - sage: bmilp.solve([bmilp._x[permutation1010root, 1] <= 0.5], force_new_solution=True) + sage: bmilp.solve([bmilp._x[permutation1010root, 1] <= 0.5], solution_index=1) Traceback (most recent call last): ... MIPSolverException: GLPK: Problem has no feasible solution From 1ac09787fd03c114725a775cdf22b6e14f108446 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Thu, 22 Dec 2022 00:38:46 +0100 Subject: [PATCH 10/51] add some documentation and doctests, slightly simplify code --- src/sage/combinat/bijectionist.py | 268 +++++++++++++++++++----------- 1 file changed, 175 insertions(+), 93 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 75e904df59e..cb96c2ee157 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -184,9 +184,14 @@ ( [ /\/\ / \ ] [ \ / ] ) ( [ / \, / \ ], [ o o ] ) - TESTS: + The output is in a form suitable for FindStat:: - The following failed before commit c6d4d2e8804aa42afa08c72c887d50c725cc1a91:: + sage: findmap(list(bij.minimal_subdistributions_iterator())) # optional -- internet + 0: Mp00034 (quality [100]) + 1: Mp00061oMp00023 (quality [100]) + 2: Mp00018oMp00140 (quality [100]) + + TESTS:: sage: N=4; A = B = [permutation for n in range(N) for permutation in Permutations(n)] sage: theta = lambda pi: Permutation([x+1 if x != len(pi) else 1 for x in pi[-1:]+pi[:-1]]) @@ -373,11 +378,6 @@ # Z. In LaTeX, we mostly call them \tilde A, \tilde Z, etc. now. It # would be good to have a standard name in code, too. -# TODO: (medium) whenever possible, doctests of a method should only -# test this method. Currently we have very many system tests, which -# is inconvenient when modifying the design substantially. - - class Bijectionist(SageObject): r""" A toolbox to list all possible bijections between two finite sets @@ -722,7 +722,7 @@ def set_statistics(self, *alpha_beta): """ self.bmilp = None self._n_statistics = len(alpha_beta) - # TODO: (low) do we really want to recompute statistics every time? + # TODO: do we really want to recompute statistics every time? self._alpha = lambda p: tuple(arg[0](p) for arg in alpha_beta) self._beta = lambda p: tuple(arg[1](p) for arg in alpha_beta) @@ -1047,6 +1047,21 @@ def _compute_possible_block_values(self): Update the dictionary of possible values of each block. This has to be called whenever ``self._P`` was modified. + + It raises a :class:`ValueError`, if the restrictions on a + block are contradictory. + + TESTS:: + + sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] + sage: tau = Permutation.longest_increasing_subsequence_length + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_value_restrictions((Permutation([1, 2]), [4, 5])) + sage: bij._compute_possible_block_values() + Traceback (most recent call last): + ... + ValueError: No possible values found for singleton block [[1, 2]] + """ self._possible_block_values = {} # P -> Power(Z) for p, block in self._P.root_to_elements_dict().items(): @@ -1769,7 +1784,6 @@ def minimal_subdistributions_iterator(self): # the current solution minimal_subdistribution.add_constraint(sum(active_vars) <= len(active_vars) - 1, name="veto") - # TODO: can we ignore that in the next step the same constraint is added again? else: s = new_s @@ -2020,14 +2034,16 @@ def _find_counter_example2(self, bmilp, P, s0, d): def _preprocess_intertwining_relations(self): r""" - - .. TODO:: - - (medium) untangle side effect and return value if possible - Make `self._P` be the finest set partition coarser than `self._P` such that composing elements preserves blocks. + OUTPUT: + + A list of triples `((\pi/\rho, p, (p_1,\dots,p_k))`, where + `p` is the block of `\rho(s(a_1),\dots, s(a_k))`, for any + `a_i\in p_i`, suitable for + :meth:`_BijectionistMILP.add_intertwining_relation_constraints`. + Suppose that `p_1`, `p_2` are blocks of `P`, and `a_1, a'_1 \in p_1` and `a_2, a'_2\in p_2`. Then, @@ -2049,6 +2065,10 @@ def _preprocess_intertwining_relations(self): create one test with one and one test with two intertwining_relations + .. TODO:: + + untangle side effect and return value if possible + """ images = {} # A^k -> A, a_1,...,a_k to pi(a_1,...,a_k), for all pi origins_by_elements = [] # (pi/rho, pi(a_1,...,a_k), a_1,...,a_k) @@ -2060,9 +2080,9 @@ def _preprocess_intertwining_relations(self): if a in self._A: if a in images: # this happens if there are several pi's of the same arity - images[a_tuple].add(a) # TODO: (low) wouldn't self._P.find(a) be more efficient here? + images[a_tuple].add(a) # TODO: wouldn't self._P.find(a) be more efficient here? else: - images[a_tuple] = set((a,)) # TODO: (low) wouldn't self._P.find(a) be more efficient here? + images[a_tuple] = set((a,)) # TODO: wouldn't self._P.find(a) be more efficient here? origins_by_elements.append((composition_index, a, a_tuple)) # merge blocks @@ -2483,7 +2503,7 @@ def _solution(self, bmilp): map = {} # A -> Z, a +-> s(a) for p, block in self._P.root_to_elements_dict().items(): for z in self._possible_block_values[p]: - if bmilp.get_value(p, z) == 1: + if bmilp.has_value(p, z): for a in block: map[a] = z break @@ -2498,7 +2518,7 @@ def _solution_by_blocks(self, bmilp): map = {} # P -> Z, a +-> s(a) for p in _disjoint_set_roots(self._P): for z in self._possible_block_values[p]: - if bmilp.get_value(p, z) == 1: + if bmilp.has_value(p, z): map[p] = z break return map @@ -2547,8 +2567,8 @@ def _show_bmilp(self, bmilp, variables=True): def _generate_and_solve_initial_bmilp(self): r""" - Generate a ``_BijectionistMILP``, add all relevant constraints - and call ``MILP.solve()``. + Generate a :class:`_BijectionistMILP`, add all relevant constraints + and call :meth:`_BijectionistMILP.solve`. """ preimage_blocks = self._preprocess_intertwining_relations() self._compute_possible_block_values() @@ -2557,7 +2577,7 @@ def _generate_and_solve_initial_bmilp(self): n = bmilp.milp.number_of_variables() bmilp.add_alpha_beta_constraints() bmilp.add_distribution_constraints() - bmilp.add_interwining_relation_constraints(preimage_blocks) + bmilp.add_intertwining_relation_constraints(preimage_blocks) if get_verbose() >= 2: self._show_bmilp(bmilp) assert n == bmilp.milp.number_of_variables(), "The number of variables increased." @@ -2565,14 +2585,28 @@ def _generate_and_solve_initial_bmilp(self): return bmilp -class _BijectionistMILP(SageObject): +class _BijectionistMILP(): r""" - Wrapper class for the MixedIntegerLinearProgram (MILP). This class is used to manage the MILP, - add constraints, solve the problem and check for uniqueness of solution values. + Wrapper class for the MixedIntegerLinearProgram (MILP). This + class is used to manage the MILP, add constraints, solve the + problem and check for uniqueness of solution values. + """ def __init__(self, bijectionist: Bijectionist): - # TODO: it would be cleaner not to pass the full bijectionist - # instance, but only those attributes we actually use: + r""" + Initialize the mixed integer linear program. + + INPUT: + + - ``bijectionist`` -- an instance of :class:`Bijectionist`. + + .. TODO:: + + it might be cleaner not to pass the full bijectionist + instance, but only those attributes we actually use + + """ + # the attributes of the bijectionist class we actually use: # _possible_block_values # _elements_distributions # _W, _Z, _A, _B, _P, _alpha, _beta, _tau, _pi_rho @@ -2592,68 +2626,91 @@ def __init__(self, bijectionist: Bijectionist): for z in bijectionist._possible_block_values[p]) == 1, name=name[:50]) - def solve(self, tmp_constraints, solution_index=0): + def solve(self, additional_constraints, solution_index=0): + r""" + Return a solution satisfying the given additional constraints. + + INPUT: + + - ``additional_constraints`` -- a list of constraints for the + underlying MILP + + - ``solution_index`` (optional, default: ``0``) -- an index + specifying how many of the solutions in the cache should be + ignored. + + """ if self._n_variables < 0: + # initialize at first call self._n_variables = self.milp.number_of_variables() self._index_block_value_dict = {} for (p, z), v in self._x.items(): variable_index = next(iter(v.dict().keys())) self._index_block_value_dict[variable_index] = (p, z) - # number of variables would change with creation of constraints with new variables + # number of variables would change with creation of + # constraints with new variables assert self._n_variables == self.milp.number_of_variables(), "The number of variables changed." - # check if previous solution exists with constraints - previous_solution_exists = False + # check if there is a solution satisfying the constraints in + # the cache for solution in self._solution_cache[solution_index:]: - fulfills_constraints = True - # loop through all constraints - for constraint in tmp_constraints: - # check equations - for linear_function, value in constraint.equations(): - solution_value = self._evaluate_linear_function(linear_function.dict(), self._index_block_value_dict, solution) - if solution_value != value.dict()[-1]: - fulfills_constraints = False - break - if not fulfills_constraints: - break - # check inequalities - for linear_function, value in constraint.inequalities(): - solution_value = self._evaluate_linear_function(linear_function.dict(), self._index_block_value_dict, solution) - if solution_value > value.dict()[-1]: - fulfills_constraints = False - break - if not fulfills_constraints: - break - if fulfills_constraints: - previous_solution_exists = True + if all(all(self._evaluate_linear_function(linear_function, + solution) == value.dict()[-1] + for linear_function, value in constraint.equations()) + and all(self._evaluate_linear_function(linear_function, + solution) <= value.dict()[-1] + for linear_function, value in constraint.inequalities()) + for constraint in additional_constraints): self.last_solution = solution - break - - # if no previous solution satisfies the constraints, generate a new one - if not previous_solution_exists: - try: - n_constraints = self.milp.number_of_constraints() - for constraint in tmp_constraints: - self.milp.add_constraint(constraint) - self.milp.solve() - for _ in range(self.milp.number_of_constraints()-n_constraints): - self.milp.remove_constraint(n_constraints) - except MIPSolverException as error: - for _ in range(self.milp.number_of_constraints()-n_constraints): - self.milp.remove_constraint(n_constraints) - raise error - - self.last_solution = self.milp.get_values(self._x) - self._solution_cache.append(self.last_solution) - self.veto_current_solution() + return self.last_solution + # otherwise generate a new one + try: + # TODO: wouldn't it be safer to copy the milp? + n_constraints = self.milp.number_of_constraints() + for constraint in additional_constraints: + self.milp.add_constraint(constraint) + self.milp.solve() + for _ in range(self.milp.number_of_constraints()-n_constraints): + self.milp.remove_constraint(n_constraints) + except MIPSolverException as error: + for _ in range(self.milp.number_of_constraints()-n_constraints): + self.milp.remove_constraint(n_constraints) + raise error + + self.last_solution = self.milp.get_values(self._x) + self._solution_cache.append(self.last_solution) + self._veto_current_solution() return self.last_solution - def _evaluate_linear_function(self, linear_function_dict, block_index_dict, values): - return float(sum(linear_function_dict[index]*values[block_index_dict[index]] for index in linear_function_dict)) + def _evaluate_linear_function(self, linear_function, values): + r""" + Evaluate the given function at the given values. + + INPUT: + + - ``linear_function``, a + :class:`sage.numerical.linear_functions.LinearFunction`. - def get_value(self, p, v): - return self.last_solution[p, v] + - ``values``, a candidate for a solution of the MILP as a + dictionary from pairs `(a, z)\in A\times Z` to `0` or `1`, + specifying whether `a` is mapped to `z`. + + """ + return float(sum(value * values[self._index_block_value_dict[index]] + for index, value in linear_function.dict().items())) + + def has_value(self, p, v): + r""" + Return whether a block is mapped to a value in the last solution + computed. + + INPUT: + + - ``p``, the representative of a block + - ``v``, a value in `Z` + """ + return self.last_solution[p, v] == 1 def add_alpha_beta_constraints(self): r""" @@ -2690,8 +2747,8 @@ def add_alpha_beta_constraints(self): z_index = Z_dict[self._bijectionist._tau[b]] B_matrix[z_index][w_index] += 1 - # TODO: (low) I am not sure that this is the best way to - # filter out empty conditions + # TODO: not sure that this is the best way to filter out + # empty conditions for w in range(len(W)): for z in range(len(Z)): c = AZ_matrix[z][w] - B_matrix[z][w] @@ -2704,7 +2761,7 @@ def add_alpha_beta_constraints(self): def add_distribution_constraints(self): r""" Add constraints so the distributions given by - :meth:`~Bijectionist.set_distributions` are fulfilled. + :meth:`set_distributions` are fulfilled. To accomplish this we add @@ -2728,8 +2785,8 @@ def add_distribution_constraints(self): for z in values: values_sum[Z_dict[z]] += 1 - # TODO: (low) I am not sure that this is the best way to - # filter out empty conditions + # TODO: not sure that this is the best way to filter out + # empty conditions for element, value in zip(elements_sum, values_sum): c = element - value if c.is_zero(): @@ -2738,13 +2795,19 @@ def add_distribution_constraints(self): raise MIPSolverException self.milp.add_constraint(c == 0, name=f"d: {element} == {value}") - def add_interwining_relation_constraints(self, origins): + def add_intertwining_relation_constraints(self, origins): r""" Add constraints corresponding to the given intertwining relations. + INPUT: + + - origins, a list of triples `((\pi/\rho, p, + (p_1,\dots,p_k))`, where `p` is the block of + `\rho(s(a_1),\dots, s(a_k))`, for any `a_i\in p_i`. + This adds the constraints imposed by - :meth:`~Bijectionist.set_intertwining_relations`. + :meth:`set_intertwining_relations`, .. MATH:: @@ -2771,15 +2834,8 @@ def add_interwining_relation_constraints(self, origins): x_{p, z}\geq 1 - k + \sum_{i=1}^k x_{p_i, z_i}. - Not that `z` must be a possible value of `p` and each `z_i` + Note that `z` must be a possible value of `p` and each `z_i` must be a possible value of `p_i`. - - INPUT: - - - origins, a list of triples `((\pi/\rho, p, - (p_1,\dots,p_k))`, where `p` is the block of - `\rho(s(a_1),\dots, s(a_k))`, for any `a_i\in p_i`. - """ for composition_index, image_block, preimage_blocks in origins: pi_rho = self._bijectionist._pi_rho[composition_index] @@ -2799,14 +2855,13 @@ def add_interwining_relation_constraints(self, origins): self.milp.add_constraint(rhs <= 0, name=f"pi/rho({composition_index})") - def veto_current_solution(self): + def _veto_current_solution(self): r""" Add a constraint vetoing the current solution. This adds a constraint such that the next call to - :meth:`MixedIntegerLinearProgram.solve()` must return a - solution different from the current one. - + :meth:`solve` must return a solution different from the + current one. We require that the MILP currently has a solution. @@ -2837,6 +2892,19 @@ def _invert_dict(d): """ Return the dictionary whose keys are the values of the input and whose values are the lists of preimages. + + INPUT: + + - ``d``, a ``dict``. + + EXAMPLES:: + + sage: from sage.combinat.bijectionist import _invert_dict + sage: _invert_dict({1: "a", 2: "a", 3:"b"}) + {'a': [1, 2], 'b': [3]} + + sage: _invert_dict({}) + {} """ preimages = {} for k, v in d.items(): @@ -2847,6 +2915,20 @@ def _invert_dict(d): def _disjoint_set_roots(d): """ Return the representatives of the blocks of the disjoint set. + + INPUT: + + - ``d``, a ``sage.sets.disjoint_set.DisjointSet_of_hashables`` + + EXAMPLES:: + + sage: from sage.combinat.bijectionist import _disjoint_set_roots + sage: d = DisjointSet('abcde') + sage: d.union("a", "b") + sage: d.union("a", "c") + sage: d.union("e", "d") + sage: _disjoint_set_roots(d) + dict_keys(['a', 'e']) """ return d.root_to_elements_dict().keys() From 47945ac2bbdbd927a6aaef8c72ead62d2922c94c Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Thu, 22 Dec 2022 01:05:46 +0100 Subject: [PATCH 11/51] add missing documentation in table of contents --- src/sage/combinat/bijectionist.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index cb96c2ee157..d8a9e548442 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -15,20 +15,20 @@ :widths: 30, 70 :delim: | - :meth:`~Bijectionist.set_intertwining_relations` | Set - :meth:`~Bijectionist.set_constant_blocks` | Set - :meth:`~Bijectionist.set_statistics` | Set - :meth:`~Bijectionist.set_value_restrictions` | Set - :meth:`~Bijectionist.set_distributions` | Set - - :meth:`~Bijectionist.statistics_table` | Return - :meth:`~Bijectionist.statistics_fibers` | Return - - :meth:`~Bijectionist.constant_blocks` | Return - :meth:`~Bijectionist.solutions_iterator` | Return - :meth:`~Bijectionist.possible_values` | Return - :meth:`~Bijectionist.minimal_subdistributions_iterator` | Return - :meth:`~Bijectionist.minimal_subdistributions_blocks_iterator` | Return + :meth:`~Bijectionist.set_intertwining_relations` | Declare that the statistic intertwines with other maps. + :meth:`~Bijectionist.set_constant_blocks` | Declare that the statistic is constant on some sets. + :meth:`~Bijectionist.set_statistics` | Declare statistics that are preserved by the bijection. + :meth:`~Bijectionist.set_value_restrictions` | Restrict the values of the statistic on an element. + :meth:`~Bijectionist.set_distributions` | Restrict the distribution of values of the statistic on some elements. + + :meth:`~Bijectionist.statistics_table` | Print a table collecting information on the given statistics. + :meth:`~Bijectionist.statistics_fibers` | Collect elements with the same statistics. + + :meth:`~Bijectionist.constant_blocks` | Return the blocks on which the statistic is constant. + :meth:`~Bijectionist.solutions_iterator` | Iterate over all possible solutions. + :meth:`~Bijectionist.possible_values` | Return all possible values for a given element. + :meth:`~Bijectionist.minimal_subdistributions_iterator` | Iterate over the minimal subdistributions. + :meth:`~Bijectionist.minimal_subdistributions_blocks_iterator` | Iterate over the minimal subdistributions. A guided tour ============= From 19c3d8fdc82471993ab800923eaeb2494c7551c7 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Thu, 22 Dec 2022 11:04:09 +0100 Subject: [PATCH 12/51] mark doctests as long, slightly simplify logic --- src/sage/combinat/bijectionist.py | 200 ++++++++++++++++-------------- 1 file changed, 110 insertions(+), 90 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index d8a9e548442..7f3bda41982 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -121,7 +121,7 @@ There is no rotation invariant statistic on non crossing set partitions which is equidistributed with the Strahler number on ordered trees:: - sage: N=8; As = [[SetPartition(d.to_noncrossing_partition()) for d in DyckWords(n)] for n in range(N)] + sage: N = 8; As = [[SetPartition(d.to_noncrossing_partition()) for d in DyckWords(n)] for n in range(N)] sage: A = sum(As, []) sage: B = sum([list(OrderedTrees(n)) for n in range(1, N+1)], []) sage: theta = lambda m: SetPartition([[i % m.size() + 1 for i in b] for b in m]) @@ -186,14 +186,14 @@ The output is in a form suitable for FindStat:: - sage: findmap(list(bij.minimal_subdistributions_iterator())) # optional -- internet + sage: findmap(list(bij.minimal_subdistributions_iterator())) # optional -- internet 0: Mp00034 (quality [100]) 1: Mp00061oMp00023 (quality [100]) 2: Mp00018oMp00140 (quality [100]) TESTS:: - sage: N=4; A = B = [permutation for n in range(N) for permutation in Permutations(n)] + sage: N = 4; A = B = [permutation for n in range(N) for permutation in Permutations(n)] sage: theta = lambda pi: Permutation([x+1 if x != len(pi) else 1 for x in pi[-1:]+pi[:-1]]) sage: def tau(pi): ....: n = len(pi) @@ -479,7 +479,7 @@ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], pi_rho=tuple(), ele Check that large input sets are handled well:: sage: A = B = list(range(20000)) - sage: bij = Bijectionist(A, B) # long time + sage: bij = Bijectionist(A, B) # long time """ # glossary of standard letters: # A, B, Z, W ... finite sets @@ -1471,8 +1471,6 @@ def _forced_constant_blocks(self): sage: all(s[Permutation([2, 1])] == s[Permutation([1, 4, 2, 3])] for s in bij.solutions_iterator()) False - - sage: A = B = ["a", "b", "c", "d", "e", "f"] sage: tau = {"a": 1, "b": 1, "c": 3, "d": 4, "e": 5, "f": 6}.get sage: bij = Bijectionist(A, B, tau) @@ -1502,7 +1500,8 @@ def _forced_constant_blocks(self): """ if self.bmilp is None: - self.bmilp = self._generate_and_solve_initial_bmilp() # may throw Exception + self.bmilp = self._initialize_new_bmilp() + self.bmilp.solve([]) # generate blockwise preimage to determine which blocks have the same image solution = self._solution_by_blocks(self.bmilp) @@ -1668,7 +1667,9 @@ def add_solution(solutions, solution): # generate initial solution, solution dict and add solution if self.bmilp is None: - self.bmilp = self._generate_and_solve_initial_bmilp() + self.bmilp = self._initialize_new_bmilp() + self.bmilp.solve([]) + solution = self._solution(self.bmilp) solutions = {} add_solution(solutions, solution) @@ -1759,7 +1760,8 @@ def minimal_subdistributions_iterator(self): try: if self.bmilp is None: - self.bmilp = self._generate_and_solve_initial_bmilp() + self.bmilp = self._initialize_new_bmilp() + self.bmilp.solve([]) except MIPSolverException: return s = self._solution(self.bmilp) @@ -1960,11 +1962,12 @@ def add_counter_example_constraint(s): minimal_subdistribution.add_constraint(sum(D[p] for p in P if s[p] == v) == V[v]) - try: - if self.bmilp is None: - self.bmilp = self._generate_and_solve_initial_bmilp() - except MIPSolverException: - return + if self.bmilp is None: + try: + self.bmilp = self._initialize_new_bmilp() + self.bmilp.solve([]) + except MIPSolverException: + return s = self._solution_by_blocks(self.bmilp) add_counter_example_constraint(s) while True: @@ -2362,6 +2365,7 @@ def solutions_iterator(self): sage: iterator2 = bij.solutions_iterator() Generate a solution in iterator1, iterator2 should generate the same solution and vice versa:: + sage: next(iterator1) {'a1': 1, 'a2': 1, @@ -2470,29 +2474,24 @@ def solutions_iterator(self): 'b8': 1, 'd': 0} """ - bmilp = None next_solution = None - try: - if self.bmilp is None: - self.bmilp = self._generate_and_solve_initial_bmilp() - bmilp = self.bmilp - bmilp.solve([], 0) - next_solution = self._solution(bmilp) - except MIPSolverException: - return - - solution_index = 1 + if self.bmilp is None: + try: + self.bmilp = self._initialize_new_bmilp() + except MIPSolverException: + return + bmilp = self.bmilp + solution_index = 0 while True: - yield next_solution - if get_verbose() >= 2: - print("after vetoing") - self._show_bmilp(bmilp, variables=False) try: bmilp.solve([], solution_index) - next_solution = self._solution(bmilp) - solution_index += 1 except MIPSolverException: return + yield self._solution(bmilp) + solution_index += 1 + if get_verbose() >= 2: + print("after vetoing") + self._show_bmilp(bmilp, variables=False) def _solution(self, bmilp): """ @@ -2565,10 +2564,9 @@ def _show_bmilp(self, bmilp, variables=True): print(f" {v}: " + "".join([f"s({a}) = " for a in self._P.root_to_elements_dict()[p]]) + f"{z}") - def _generate_and_solve_initial_bmilp(self): + def _initialize_new_bmilp(self): r""" - Generate a :class:`_BijectionistMILP`, add all relevant constraints - and call :meth:`_BijectionistMILP.solve`. + Initialize a :class:`_BijectionistMILP` and add the current constraints. """ preimage_blocks = self._preprocess_intertwining_relations() self._compute_possible_block_values() @@ -2581,7 +2579,6 @@ def _generate_and_solve_initial_bmilp(self): if get_verbose() >= 2: self._show_bmilp(bmilp) assert n == bmilp.milp.number_of_variables(), "The number of variables increased." - bmilp.solve([]) return bmilp @@ -2605,6 +2602,13 @@ def __init__(self, bijectionist: Bijectionist): it might be cleaner not to pass the full bijectionist instance, but only those attributes we actually use + TESTS:: + + sage: A = B = ["a", "b", "c", "d"] + sage: bij = Bijectionist(A, B) + sage: from sage.combinat.bijectionist import _BijectionistMILP + sage: _BijectionistMILP(bij) + """ # the attributes of the bijectionist class we actually use: # _possible_block_values @@ -2639,7 +2643,72 @@ def solve(self, additional_constraints, solution_index=0): specifying how many of the solutions in the cache should be ignored. + TESTS:: + + sage: A = B = ["a", "b"] + sage: bij = Bijectionist(A, B) + sage: from sage.combinat.bijectionist import _BijectionistMILP + sage: bmilp = _BijectionistMILP(bij) + sage: len(bmilp._solution_cache) + 0 + + Without any constraints, we do not require that the solution is a bijection:: + + sage: bmilp.solve([bmilp._x["a", "a"] == 1, bmilp._x["b", "a"] == 1]) + {('a', 'a'): 1.0, ('a', 'b'): 0.0, ('b', 'a'): 1.0, ('b', 'b'): 0.0} + sage: len(bmilp._solution_cache) + 1 + sage: bmilp.solve([bmilp._x["a", "b"] == 1, bmilp._x["b", "b"] == 1]) + {('a', 'a'): 0.0, ('a', 'b'): 1.0, ('b', 'a'): 0.0, ('b', 'b'): 1.0} + sage: len(bmilp._solution_cache) + 2 + + A more elaborate test:: + + sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)] + sage: tau = lambda D: D.number_of_touch_points() + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength())) + sage: bmilp = bij._initialize_new_bmilp() + + Generate a solution:: + + sage: bmilp.solve([]) + {([], 0): 1.0, + ([1, 0], 1): 1.0, + ([1, 0, 1, 0], 1): 0.0, + ([1, 0, 1, 0], 2): 1.0, + ([1, 1, 0, 0], 1): 1.0, + ([1, 1, 0, 0], 2): 0.0} + + Generating a new solution that also maps `1010` to `2` fails: + + sage: bmilp.solve([bmilp._x[DyckWord([1,0,1,0]), 1] <= 0.5], solution_index=1) + Traceback (most recent call last): + ... + MIPSolverException: GLPK: Problem has no feasible solution + + However, searching for a cached solution succeeds, for inequalities and equalities:: + + sage: bmilp.solve([bmilp._x[DyckWord([1,0,1,0]), 1] <= 0.5]) + {([], 0): 1.0, + ([1, 0], 1): 1.0, + ([1, 0, 1, 0], 1): 0.0, + ([1, 0, 1, 0], 2): 1.0, + ([1, 1, 0, 0], 1): 1.0, + ([1, 1, 0, 0], 2): 0.0} + + sage: bmilp.solve([bmilp._x[DyckWord([1,0,1,0]), 1] == 0]) + {([], 0): 1.0, + ([1, 0], 1): 1.0, + ([1, 0, 1, 0], 1): 0.0, + ([1, 0, 1, 0], 2): 1.0, + ([1, 1, 0, 0], 1): 1.0, + ([1, 1, 0, 0], 2): 0.0} + """ + assert 0 <= solution_index <= len(self._solution_cache), "the index of the desired solution must not be larger than the number of known solutions" + if self._n_variables < 0: # initialize at first call self._n_variables = self.milp.number_of_variables() @@ -2964,55 +3033,6 @@ def _non_copying_intersection(sets): """ TESTS:: - ##################### - # caching base test # - ##################### - sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)] - sage: tau = lambda D: D.number_of_touch_points() - sage: bij = Bijectionist(A, B, tau) - sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength())) - sage: bmilp = bij._generate_and_solve_initial_bmilp() - -Print the generated solution:: - - sage: bmilp.milp.get_values(bmilp._x) - {([], 0): 1.0, - ([1, 0], 1): 1.0, - ([1, 0, 1, 0], 1): 0.0, - ([1, 0, 1, 0], 2): 1.0, - ([1, 1, 0, 0], 1): 1.0, - ([1, 1, 0, 0], 2): 0.0} - -Generating a new solution that also maps `1010` to `2` fails: - - sage: from sage.combinat.bijectionist import _disjoint_set_roots - sage: permutation1010root = list(_disjoint_set_roots(bij._P))[2] - sage: permutation1010root - [1, 0, 1, 0] - sage: bmilp.solve([bmilp._x[permutation1010root, 1] <= 0.5], solution_index=1) - Traceback (most recent call last): - ... - MIPSolverException: GLPK: Problem has no feasible solution - -However, searching for a cached solution succeeds, for inequalities and equalities:: - - sage: bmilp.solve([bmilp._x[permutation1010root, 1] <= 0.5]) - {([], 0): 1.0, - ([1, 0], 1): 1.0, - ([1, 0, 1, 0], 1): 0.0, - ([1, 0, 1, 0], 2): 1.0, - ([1, 1, 0, 0], 1): 1.0, - ([1, 1, 0, 0], 2): 0.0} - - sage: bmilp.solve([bmilp._x[permutation1010root, 1] == 0]) - {([], 0): 1.0, - ([1, 0], 1): 1.0, - ([1, 0, 1, 0], 1): 0.0, - ([1, 0, 1, 0], 2): 1.0, - ([1, 1, 0, 0], 1): 1.0, - ([1, 1, 0, 0], 2): 0.0} - - sage: As = Bs = [[], ....: [(1,i,j) for i in [-1,0,1] for j in [-1,1]], ....: [(2,i,j) for i in [-1,0,1] for j in [-1,1]], @@ -3027,7 +3047,7 @@ def _non_copying_intersection(sets): sage: bij = Bijectionist(sum(As, []), sum(Bs, [])) sage: bij.set_statistics((lambda x: x[0], lambda x: x[0])) sage: bij.set_intertwining_relations((2, c1, c1), (1, c2, c2)) - sage: l = list(bij.solutions_iterator()); len(l) + sage: l = list(bij.solutions_iterator()); len(l) # long time 64 A brute force check would be difficult:: @@ -3055,8 +3075,8 @@ def _non_copying_intersection(sets): sage: A = sum(As, []) sage: respects_c1 = lambda s: all(c1(a1, a2) not in A or s[c1(a1, a2)] == c1(s[a1], s[a2]) for a1 in A for a2 in A) sage: respects_c2 = lambda s: all(c2(a1) not in A or s[c2(a1)] == c2(s[a1]) for a1 in A) - sage: l2 = [s for s in it if respects_c1(s) and respects_c2(s)] - sage: sorted(l1, key=lambda s: tuple(s.items())) == l2 + sage: l2 = [s for s in it if respects_c1(s) and respects_c2(s)] # long time + sage: sorted(l1, key=lambda s: tuple(s.items())) == l2 # long time True Our benchmark example:: @@ -3071,7 +3091,7 @@ def _non_copying_intersection(sets): ....: cycle = Permutation(tuple(range(1, len(p)+1))) ....: return Permutation([cycle.inverse()(p(cycle(i))) for i in range(1, len(p)+1)]) - sage: N=5 + sage: N = 5 sage: As = [list(Permutations(n)) for n in range(N+1)] sage: A = B = sum(As, []) sage: bij = Bijectionist(A, B, gamma) @@ -3103,9 +3123,9 @@ def _non_copying_intersection(sets): ([[2, 1, 5, 3, 4], [2, 5, 1, 3, 4], [3, 1, 5, 2, 4], [3, 5, 1, 2, 4]], [3, 3, 4, 4]) ([[1, 3, 2, 5, 4], [1, 3, 5, 2, 4], [1, 4, 2, 5, 3], [1, 4, 5, 2, 3], [1, 4, 5, 3, 2], [1, 5, 4, 2, 3], [1, 5, 4, 3, 2]], [2, 2, 3, 3, 3, 3, 3]) - sage: l = list(bij.solutions_iterator()); len(l) # long time + sage: l = list(bij.solutions_iterator()); len(l) # long time 504 - sage: for a, d in bij.minimal_subdistributions_iterator(): # long time + sage: for a, d in bij.minimal_subdistributions_iterator(): # long time ....: print(sorted(a), sorted(d)) """ From eca857ebb1c8f29abd4e7afb7f386f98b18584d5 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Thu, 22 Dec 2022 23:25:34 +0100 Subject: [PATCH 13/51] slightly simplify, more doctests --- src/sage/combinat/bijectionist.py | 232 ++++++++++++++---------------- 1 file changed, 108 insertions(+), 124 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 7f3bda41982..26a93072685 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -5,7 +5,7 @@ AUTHORS: - Alexander Grosz, Tobias Kietreiber, Stephan Pfannerer and Martin - Rubey (2020): Initial version + Rubey (2020-2022): Initial version Quick reference =============== @@ -1504,26 +1504,29 @@ def _forced_constant_blocks(self): self.bmilp.solve([]) # generate blockwise preimage to determine which blocks have the same image - solution = self._solution_by_blocks(self.bmilp) + solution = self._solution(self.bmilp, True) multiple_preimages = {(value,): preimages for value, preimages in _invert_dict(solution).items() if len(preimages) > 1} - # check for each pair of blocks if a solution with different values on these block exists - # if yes, use the new solution to update the multiple_preimages dictionary, restart the check + # check for each pair of blocks if a solution with different + # values on these block exists + + # if yes, use the new solution to update the + # multiple_preimages dictionary, restart the check # if no, the two blocks can be joined # _P has to be copied to not mess with the solution-process - # since we do not want to regenerate the bmilp in each step, so blocks - # have to stay consistent during the whole process + # since we do not want to regenerate the bmilp in each step, + # so blocks have to stay consistent during the whole process tmp_P = deepcopy(self._P) updated_preimages = True while updated_preimages: updated_preimages = False - for values in copy(multiple_preimages): # copy to be able to modify dict + for values in copy(multiple_preimages): if updated_preimages: break - for i, j in itertools.combinations(copy(multiple_preimages[values]), r=2): # copy to be able to modify list + for i, j in itertools.combinations(copy(multiple_preimages[values]), r=2): tmp_constraints = [] try: # veto the two blocks having the same value @@ -1533,11 +1536,11 @@ def _forced_constant_blocks(self): self.bmilp.solve(tmp_constraints) # solution exists, update dictionary - solution = self._solution_by_blocks(self.bmilp) + solution = self._solution(self.bmilp, True) updated_multiple_preimages = {} for values in multiple_preimages: for p in multiple_preimages[values]: - solution_tuple = (*values, solution[p]) # tuple so actual solutions were equal in lookup + solution_tuple = (*values, solution[p]) if solution_tuple not in updated_multiple_preimages: updated_multiple_preimages[solution_tuple] = [] updated_multiple_preimages[solution_tuple].append(p) @@ -1547,7 +1550,8 @@ def _forced_constant_blocks(self): except MIPSolverException: # no solution exists, join blocks tmp_P.union(i, j) - if i in multiple_preimages[values] and j in multiple_preimages[values]: # only one of the joined blocks should remain in the list + if i in multiple_preimages[values] and j in multiple_preimages[values]: + # only one of the joined blocks should remain in the list multiple_preimages[values].remove(j) if len(multiple_preimages[values]) == 1: del multiple_preimages[values] @@ -1773,7 +1777,7 @@ def minimal_subdistributions_iterator(self): except MIPSolverException: return d = minimal_subdistribution.get_values(D) # a dict from A to {0, 1} - new_s = self._find_counter_example(self.bmilp, s, d) + new_s = self._find_counter_example(self._A, s, d, False) if new_s is None: values = self._sorter["Z"](s[a] for a in self._A if d[a]) yield ([a for a in self._A if d[a]], values) @@ -1789,31 +1793,35 @@ def minimal_subdistributions_iterator(self): else: s = new_s - def _find_counter_example(self, bmilp, s0, d): + def _find_counter_example(self, P, s0, d, on_blocks): r""" Return a solution `s` such that ``d`` is not a subdistribution of `s0`. - TODO: better name - INPUT: - - ``bmilp``, the mixed linear integer program + - ``P``, the representatives of the blocks, or `A` if + ``on_blocks`` is ``False``. + + - ``s0``, a solution. - - ``s0``, a solution + - ``d``, a subset of `A`, in the form of a dict from `A` to `\{0, 1\}`. + + - ``on_blocks``, whether to return the counter example on + blocks or on elements. - - ``d``, a subset of `A`, in the form of a dict from `A` to `\{0, 1\}` """ + bmilp = self.bmilp for v in self._Z: - v_in_d_count = sum(d[a] for a in self._A if s0[a] == v) + v_in_d_count = sum(d[p] for p in P if s0[p] == v) if not v_in_d_count: continue # try to find a solution which has a different # subdistribution on d than s0 - v_in_d = sum(d[a] * bmilp._x[self._P.find(a), v] - for a in self._A - if v in self._possible_block_values[self._P.find(a)]) + v_in_d = sum(d[p] * bmilp._x[self._P.find(p), v] + for p in P + if v in self._possible_block_values[self._P.find(p)]) # it is sufficient to require that v occurs less often as # a value among {a | d[a] == 1} than it does in @@ -1822,11 +1830,12 @@ def _find_counter_example(self, bmilp, s0, d): tmp_constraints = [v_in_d <= v_in_d_count - 1] try: bmilp.solve(tmp_constraints) - return self._solution(bmilp) + return self._solution(bmilp, on_blocks) except MIPSolverException: pass return + def minimal_subdistributions_blocks_iterator(self): r""" Return all representatives of minimal subsets `\tilde P` @@ -1968,7 +1977,7 @@ def add_counter_example_constraint(s): self.bmilp.solve([]) except MIPSolverException: return - s = self._solution_by_blocks(self.bmilp) + s = self._solution(self.bmilp, True) add_counter_example_constraint(s) while True: try: @@ -1976,7 +1985,7 @@ def add_counter_example_constraint(s): except MIPSolverException: return d = minimal_subdistribution.get_values(D) # a dict from P to multiplicities - new_s = self._find_counter_example2(self.bmilp, P, s, d) + new_s = self._find_counter_example(P, s, d, True) if new_s is None: yield ([p for p in P for _ in range(ZZ(d[p]))], self._sorter["Z"](s[p] @@ -1991,50 +2000,6 @@ def add_counter_example_constraint(s): s = new_s add_counter_example_constraint(s) - def _find_counter_example2(self, bmilp, P, s0, d): - r""" - Return a solution `s` such that ``d`` is not a subdistribution of - `s0`. - - .. TODO:: - - find a better name - possibly not relevant if we - implement the cache of solutions - - INPUT: - - - ``bmilp``, the mixed linear integer program - - - ``P``, the representatives of the blocks - - - ``s0``, a solution - - - ``d``, a subset of `A`, in the form of a dict from `A` to `\{0, 1\}` - - """ - for v in self._Z: - v_in_d_count = sum(d[p] for p in P if s0[p] == v) - if not v_in_d_count: - continue - - # try to find a solution which has a different - # subdistribution on d than s0 - v_in_d = sum(d[p] * bmilp._x[p, v] - for p in P - if v in self._possible_block_values[p]) - - # it is sufficient to require that v occurs less often as - # a value among {a | d[a] == 1} than it does in - # v_in_d_count, because, if the distributions are - # different, one such v must exist - tmp_constraints = [v_in_d <= v_in_d_count - 1] - try: - bmilp.solve(tmp_constraints) - return self._solution_by_blocks(bmilp) - except MIPSolverException: - pass - return - def _preprocess_intertwining_relations(self): r""" Make `self._P` be the finest set partition coarser than `self._P` @@ -2493,34 +2458,30 @@ def solutions_iterator(self): print("after vetoing") self._show_bmilp(bmilp, variables=False) - def _solution(self, bmilp): - """ - Return the bmilp solution as a dictionary from `A` to - `Z`. + def _solution(self, bmilp, on_blocks=False): + r""" + Return the current solution as a dictionary from `A` (or + `P`) to `Z`. - """ - map = {} # A -> Z, a +-> s(a) - for p, block in self._P.root_to_elements_dict().items(): - for z in self._possible_block_values[p]: - if bmilp.has_value(p, z): - for a in block: - map[a] = z - break - return map + INPUT: - def _solution_by_blocks(self, bmilp): - """ - Return the bmilp solution as a dictionary from block - representatives of `P` to `Z`. + - ``bmilp``, a :class:`_BijectionistMILP`. + + - ``on_blocks``, whether to return the solution on blocks or + on all elements """ - map = {} # P -> Z, a +-> s(a) - for p in _disjoint_set_roots(self._P): + mapping = {} # A -> Z or P -> Z, a +-> s(a) + for p, block in self._P.root_to_elements_dict().items(): for z in self._possible_block_values[p]: if bmilp.has_value(p, z): - map[p] = z + if on_blocks: + mapping[p] = z + else: + for a in block: + mapping[a] = z break - return map + return mapping def _show_bmilp(self, bmilp, variables=True): """ @@ -2740,10 +2701,10 @@ def solve(self, additional_constraints, solution_index=0): for constraint in additional_constraints: self.milp.add_constraint(constraint) self.milp.solve() - for _ in range(self.milp.number_of_constraints()-n_constraints): + for _ in range(self.milp.number_of_constraints() - n_constraints): self.milp.remove_constraint(n_constraints) except MIPSolverException as error: - for _ in range(self.milp.number_of_constraints()-n_constraints): + for _ in range(self.milp.number_of_constraints() - n_constraints): self.milp.remove_constraint(n_constraints) raise error @@ -2765,10 +2726,55 @@ def _evaluate_linear_function(self, linear_function, values): dictionary from pairs `(a, z)\in A\times Z` to `0` or `1`, specifying whether `a` is mapped to `z`. + EXAMPLES:: + + sage: A = B = ["a", "b"] + sage: bij = Bijectionist(A, B) + sage: from sage.combinat.bijectionist import _BijectionistMILP + sage: bmilp = _BijectionistMILP(bij) + sage: _ = bmilp.solve([]) + sage: bmilp._index_block_value_dict # random + {0: ('a', 'a'), 1: ('a', 'b'), 2: ('b', 'a'), 3: ('b', 'b')} + sage: f = bmilp._x["a", "a"] + bmilp._x["b", "a"] + sage: v = {('a', 'a'): 1.0, ('a', 'b'): 0.0, ('b', 'a'): 1.0, ('b', 'b'): 0.0} + sage: bmilp._evaluate_linear_function(f, v) + 2.0 """ return float(sum(value * values[self._index_block_value_dict[index]] for index, value in linear_function.dict().items())) + def _veto_current_solution(self): + r""" + Add a constraint vetoing the current solution. + + This adds a constraint such that the next call to + :meth:`solve` must return a solution different from the + current one. + + We require that the MILP currently has a solution. + + .. WARNING:: + + The underlying MILP will be modified! + + ALGORITHM: + + We add the constraint `\sum_{x\in V} x < |V|`` where `V` is + the set of variables `x_{p, z}` with value 1, that is, the + set of variables indicating the current solution. + + """ + # get all variables with value 1 + active_vars = [self._x[p, z] + for p in _disjoint_set_roots(self._bijectionist._P) + for z in self._bijectionist._possible_block_values[p] + if self.milp.get_values(self._x[p, z])] + + # add constraint that not all of these can be 1, thus vetoing + # the current solution + self.milp.add_constraint(sum(active_vars) <= len(active_vars) - 1, + name="veto") + def has_value(self, p, v): r""" Return whether a block is mapped to a value in the last solution @@ -2778,6 +2784,16 @@ def has_value(self, p, v): - ``p``, the representative of a block - ``v``, a value in `Z` + + EXAMPLES:: + + sage: A = B = ["a", "b"] + sage: bij = Bijectionist(A, B) + sage: from sage.combinat.bijectionist import _BijectionistMILP + sage: bmilp = _BijectionistMILP(bij) + sage: _ = bmilp.solve([bmilp._x["a", "b"] == 1]) + sage: bmilp.has_value("a", "b") + True """ return self.last_solution[p, v] == 1 @@ -2924,38 +2940,6 @@ def add_intertwining_relation_constraints(self, origins): self.milp.add_constraint(rhs <= 0, name=f"pi/rho({composition_index})") - def _veto_current_solution(self): - r""" - Add a constraint vetoing the current solution. - - This adds a constraint such that the next call to - :meth:`solve` must return a solution different from the - current one. - - We require that the MILP currently has a solution. - - .. WARNING:: - - The underlying MILP will be modified! - - ALGORITHM: - - We add the constraint `\sum_{x\in V} x < |V|`` where `V` is - the set of variables `x_{p, z}` with value 1, that is, the - set of variables indicating the current solution. - - """ - # get all variables with value 1 - active_vars = [self._x[p, z] - for p in _disjoint_set_roots(self._bijectionist._P) - for z in self._bijectionist._possible_block_values[p] - if self.milp.get_values(self._x[p, z])] - - # add constraint that not all of these can be 1, thus vetoing - # the current solution - self.milp.add_constraint(sum(active_vars) <= len(active_vars) - 1, - name="veto") - def _invert_dict(d): """ From a04d1460832a87550ef18bb1f6767f4240d6230f Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Thu, 22 Dec 2022 23:44:38 +0100 Subject: [PATCH 14/51] doctest _find_counter_example --- src/sage/combinat/bijectionist.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 26a93072685..d8788ce68a9 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -1810,6 +1810,20 @@ def _find_counter_example(self, P, s0, d, on_blocks): - ``on_blocks``, whether to return the counter example on blocks or on elements. + EXAMPLES:: + + sage: A = B = ["a", "b", "c", "d", "e"] + sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2, "e": 3}.get + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_constant_blocks([["a", "b"]]) + sage: bij.set_value_restrictions(("a", [1, 2])) + sage: next(bij.solutions_iterator()) + {'a': 1, 'b': 1, 'c': 2, 'd': 3, 'e': 2} + + sage: s0 = {'a': 1, 'b': 1, 'c': 2, 'd': 3, 'e': 2} + sage: d = {'a': 1, 'b': 0, 'c': 0, 'd': 0, 'e': 0} + sage: bij._find_counter_example(bij._A, s0, d, False) + {'a': 2, 'b': 2, 'c': 1, 'd': 3, 'e': 1} """ bmilp = self.bmilp for v in self._Z: @@ -3107,9 +3121,9 @@ def _non_copying_intersection(sets): ([[2, 1, 5, 3, 4], [2, 5, 1, 3, 4], [3, 1, 5, 2, 4], [3, 5, 1, 2, 4]], [3, 3, 4, 4]) ([[1, 3, 2, 5, 4], [1, 3, 5, 2, 4], [1, 4, 2, 5, 3], [1, 4, 5, 2, 3], [1, 4, 5, 3, 2], [1, 5, 4, 2, 3], [1, 5, 4, 3, 2]], [2, 2, 3, 3, 3, 3, 3]) - sage: l = list(bij.solutions_iterator()); len(l) # long time + sage: l = list(bij.solutions_iterator()); len(l) # not tested 504 - sage: for a, d in bij.minimal_subdistributions_iterator(): # long time + sage: for a, d in bij.minimal_subdistributions_iterator(): # not tested ....: print(sorted(a), sorted(d)) """ From 3508426a9d10c3c904f998cdfe55447c4753cf5c Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Fri, 23 Dec 2022 00:07:50 +0100 Subject: [PATCH 15/51] doctest add_distribution_constraints and add_intertwing_relation_constraints --- src/sage/combinat/bijectionist.py | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index d8788ce68a9..568ceca83cd 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -2825,6 +2825,17 @@ def add_alpha_beta_constraints(self): as a matrix equation. + EXAMPLES:: + + sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)] + sage: bij = Bijectionist(A, B, len) + sage: bij.set_statistics((len, len)) + sage: bij._compute_possible_block_values() + sage: from sage.combinat.bijectionist import _BijectionistMILP + sage: bmilp = _BijectionistMILP(bij) + sage: bmilp.add_alpha_beta_constraints() + sage: bmilp.solve([]) + {([], 0): 1.0, ([1], 1): 1.0, ([1, 2], 2): 1.0, ([2, 1], 2): 1.0} """ W = self._bijectionist._W Z = self._bijectionist._Z @@ -2871,6 +2882,25 @@ def add_distribution_constraints(self): where `p(a)` is the block containing `a`, for each given distribution as a vector equation. + EXAMPLES:: + + sage: A = B = Permutations(3) + sage: tau = Permutation.longest_increasing_subsequence_length + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [1, 3])) + sage: bij._compute_possible_block_values() + sage: from sage.combinat.bijectionist import _BijectionistMILP + sage: bmilp = _BijectionistMILP(bij) + sage: bmilp.add_distribution_constraints() + sage: _ = bmilp.solve([]) + sage: bij._solution(bmilp) + {[1, 2, 3]: 3, + [1, 3, 2]: 1, + [2, 1, 3]: 3, + [2, 3, 1]: 3, + [3, 1, 2]: 3, + [3, 2, 1]: 3} + """ Z = self._bijectionist._Z Z_dict = {z: i for i, z in enumerate(Z)} @@ -2935,6 +2965,22 @@ def add_intertwining_relation_constraints(self, origins): Note that `z` must be a possible value of `p` and each `z_i` must be a possible value of `p_i`. + + EXAMPLES:: + + sage: A = B = list('abcd') + sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2) + sage: pi = lambda p1, p2: 'abcdefgh'[A.index(p1) + A.index(p2)] + sage: rho = lambda s1, s2: (s1 + s2) % 2 + sage: bij.set_intertwining_relations((2, pi, rho)) + sage: preimage_blocks = bij._preprocess_intertwining_relations() + sage: bij._compute_possible_block_values() + sage: from sage.combinat.bijectionist import _BijectionistMILP + sage: bmilp = _BijectionistMILP(bij) + sage: bmilp.add_intertwining_relation_constraints(preimage_blocks) + sage: _ = bmilp.solve([]) + sage: bij._solution(bmilp) + {'a': 0, 'b': 1, 'c': 0, 'd': 1} """ for composition_index, image_block, preimage_blocks in origins: pi_rho = self._bijectionist._pi_rho[composition_index] From 0ac618c968e976d19fd2c61c2462611ce8f0cc3d Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Fri, 23 Dec 2022 00:37:46 +0100 Subject: [PATCH 16/51] doctest _preprocess_intertwining_relations, _solution, _show_bmilp, _initialize_new_bmilp, _veto_current_solution --- src/sage/combinat/bijectionist.py | 146 +++++++++++++++++++++++------- 1 file changed, 112 insertions(+), 34 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 568ceca83cd..e1aaf010ea2 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -495,7 +495,7 @@ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], pi_rho=tuple(), ele assert len(A) == len(set(A)), "A must have distinct items" assert len(B) == len(set(B)), "B must have distinct items" - self.bmilp = None + self._bmilp = None self._A = A self._B = B self._sorter = {} @@ -588,7 +588,7 @@ def set_constant_blocks(self, P): MIPSolverException: ... """ - self.bmilp = None + self._bmilp = None self._P = DisjointSet(self._A) P = sorted(self._sorter["A"](p) for p in P) for p in P: @@ -720,7 +720,7 @@ def set_statistics(self, *alpha_beta): {[]: 2, [1]: 1, [1, 2]: 0, [2, 1]: 0} """ - self.bmilp = None + self._bmilp = None self._n_statistics = len(alpha_beta) # TODO: do we really want to recompute statistics every time? self._alpha = lambda p: tuple(arg[0](p) for arg in alpha_beta) @@ -1035,7 +1035,7 @@ def set_value_restrictions(self, *a_values): # of _statistics_possible_values - however, we do not want to # insist that set_value_restrictions is called after # set_statistics - self.bmilp = None + self._bmilp = None set_Z = set(self._Z) self._restrictions_possible_values = {a: set_Z for a in self._A} for a, values in a_values: @@ -1216,7 +1216,7 @@ def set_distributions(self, *elements_distributions): not in `A`. """ - self.bmilp = None + self._bmilp = None for elements, values in elements_distributions: assert len(elements) == len(values), f"{elements} and {values} are not of the same size!" for a, z in zip(elements, values): @@ -1312,7 +1312,7 @@ def set_intertwining_relations(self, *pi_rho): [] """ - self.bmilp = None + self._bmilp = None Pi_Rho = namedtuple("Pi_Rho", "numargs pi rho domain") self._pi_rho = [] @@ -1354,7 +1354,7 @@ def _forced_constant_blocks(self): sage: bij = Bijectionist(A, B, lambda x: 0) sage: bij.constant_blocks() {} - sage: bij.constant_blocks(optimal=True) + sage: bij.constant_blocks(optimal=True) # indirect doctest {{[], [1], [1, 2], [2, 1]}} In this other example we look at permutations with length 2 and 3:: @@ -1499,12 +1499,12 @@ def _forced_constant_blocks(self): {{'a', 'b'}, {'c', 'd'}} """ - if self.bmilp is None: - self.bmilp = self._initialize_new_bmilp() - self.bmilp.solve([]) + if self._bmilp is None: + self._bmilp = self._initialize_new_bmilp() + self._bmilp.solve([]) # generate blockwise preimage to determine which blocks have the same image - solution = self._solution(self.bmilp, True) + solution = self._solution(self._bmilp, True) multiple_preimages = {(value,): preimages for value, preimages in _invert_dict(solution).items() if len(preimages) > 1} @@ -1532,11 +1532,11 @@ def _forced_constant_blocks(self): # veto the two blocks having the same value for z in self._possible_block_values[i]: if z in self._possible_block_values[j]: # intersection - tmp_constraints.append(self.bmilp._x[i, z] + self.bmilp._x[j, z] <= 1) - self.bmilp.solve(tmp_constraints) + tmp_constraints.append(self._bmilp._x[i, z] + self._bmilp._x[j, z] <= 1) + self._bmilp.solve(tmp_constraints) # solution exists, update dictionary - solution = self._solution(self.bmilp, True) + solution = self._solution(self._bmilp, True) updated_multiple_preimages = {} for values in multiple_preimages: for p in multiple_preimages[values]: @@ -1670,11 +1670,11 @@ def add_solution(solutions, solution): solutions[p].add(value) # generate initial solution, solution dict and add solution - if self.bmilp is None: - self.bmilp = self._initialize_new_bmilp() - self.bmilp.solve([]) + if self._bmilp is None: + self._bmilp = self._initialize_new_bmilp() + self._bmilp.solve([]) - solution = self._solution(self.bmilp) + solution = self._solution(self._bmilp) solutions = {} add_solution(solutions, solution) @@ -1682,15 +1682,15 @@ def add_solution(solutions, solution): for p in blocks: tmp_constraints = [] for value in solutions[p]: - tmp_constraints.append(self.bmilp._x[p, value] == 0) + tmp_constraints.append(self._bmilp._x[p, value] == 0) while True: try: # problem has a solution, so new value was found - self.bmilp.solve(tmp_constraints) - solution = self._solution(self.bmilp) + self._bmilp.solve(tmp_constraints) + solution = self._solution(self._bmilp) add_solution(solutions, solution) # veto new value and try again - tmp_constraints.append(self.bmilp._x[p, solution[p]] == 0) + tmp_constraints.append(self._bmilp._x[p, solution[p]] == 0) except MIPSolverException: # no solution, so all possible values have been found break @@ -1763,12 +1763,12 @@ def minimal_subdistributions_iterator(self): minimal_subdistribution.add_constraint(sum(D[a] for a in self._A) >= 1) try: - if self.bmilp is None: - self.bmilp = self._initialize_new_bmilp() - self.bmilp.solve([]) + if self._bmilp is None: + self._bmilp = self._initialize_new_bmilp() + self._bmilp.solve([]) except MIPSolverException: return - s = self._solution(self.bmilp) + s = self._solution(self._bmilp) while True: for v in self._Z: minimal_subdistribution.add_constraint(sum(D[a] for a in self._A if s[a] == v) == V[v]) @@ -1825,7 +1825,7 @@ def _find_counter_example(self, P, s0, d, on_blocks): sage: bij._find_counter_example(bij._A, s0, d, False) {'a': 2, 'b': 2, 'c': 1, 'd': 3, 'e': 1} """ - bmilp = self.bmilp + bmilp = self._bmilp for v in self._Z: v_in_d_count = sum(d[p] for p in P if s0[p] == v) if not v_in_d_count: @@ -1985,13 +1985,13 @@ def add_counter_example_constraint(s): minimal_subdistribution.add_constraint(sum(D[p] for p in P if s[p] == v) == V[v]) - if self.bmilp is None: + if self._bmilp is None: try: - self.bmilp = self._initialize_new_bmilp() - self.bmilp.solve([]) + self._bmilp = self._initialize_new_bmilp() + self._bmilp.solve([]) except MIPSolverException: return - s = self._solution(self.bmilp, True) + s = self._solution(self._bmilp, True) add_counter_example_constraint(s) while True: try: @@ -2051,6 +2051,24 @@ def _preprocess_intertwining_relations(self): untangle side effect and return value if possible + EXAMPLES:: + + sage: A = B = list('abcd') + sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2) + sage: pi = lambda p1, p2: 'abcdefgh'[A.index(p1) + A.index(p2)] + sage: rho = lambda s1, s2: (s1 + s2) % 2 + sage: bij.set_intertwining_relations((2, pi, rho)) + sage: bij._preprocess_intertwining_relations() + {(0, 'a', ('a', 'a')), + (0, 'b', ('a', 'b')), + (0, 'b', ('b', 'a')), + (0, 'c', ('a', 'c')), + (0, 'c', ('b', 'b')), + (0, 'c', ('c', 'a')), + (0, 'd', ('a', 'd')), + (0, 'd', ('b', 'c')), + (0, 'd', ('c', 'b')), + (0, 'd', ('d', 'a'))} """ images = {} # A^k -> A, a_1,...,a_k to pi(a_1,...,a_k), for all pi origins_by_elements = [] # (pi/rho, pi(a_1,...,a_k), a_1,...,a_k) @@ -2454,12 +2472,12 @@ def solutions_iterator(self): 'd': 0} """ next_solution = None - if self.bmilp is None: + if self._bmilp is None: try: - self.bmilp = self._initialize_new_bmilp() + self._bmilp = self._initialize_new_bmilp() except MIPSolverException: return - bmilp = self.bmilp + bmilp = self._bmilp solution_index = 0 while True: try: @@ -2484,6 +2502,16 @@ def _solution(self, bmilp, on_blocks=False): - ``on_blocks``, whether to return the solution on blocks or on all elements + EXAMPLES:: + + sage: A = B = ["a", "b", "c"] + sage: bij = Bijectionist(A, B, lambda x: 0) + sage: bij.set_constant_blocks([["a", "b"]]) + sage: next(bij.solutions_iterator()) + {'a': 0, 'b': 0, 'c': 0} + sage: bij._solution(bij._bmilp, True) + {'a': 0, 'c': 0} + """ mapping = {} # A -> Z or P -> Z, a +-> s(a) for p, block in self._P.root_to_elements_dict().items(): @@ -2502,6 +2530,26 @@ def _show_bmilp(self, bmilp, variables=True): Print the constraints and variables of the current MILP together with some explanations. + + EXAMPLES:: + + sage: A = B = ["a", "b", "c"] + sage: bij = Bijectionist(A, B, lambda x: A.index(x) % 2) + sage: bij.set_constant_blocks([["a", "b"]]) + sage: next(bij.solutions_iterator()) + {'a': 0, 'b': 0, 'c': 1} + sage: bij._show_bmilp(bij._bmilp) + Constraints are: + block a: 1 <= x_0 + x_1 <= 1 + block c: 1 <= x_2 + x_3 <= 1 + statistics: 2 <= 2 x_0 + x_2 <= 2 + statistics: 1 <= 2 x_1 + x_3 <= 1 + veto: x_0 + x_3 <= 1 + Variables are: + x_0: s(a) = s(b) = 0 + x_1: s(a) = s(b) = 1 + x_2: s(c) = 0 + x_3: s(c) = 1 """ print("Constraints are:") b = bmilp.milp.get_backend() @@ -2542,6 +2590,16 @@ def _show_bmilp(self, bmilp, variables=True): def _initialize_new_bmilp(self): r""" Initialize a :class:`_BijectionistMILP` and add the current constraints. + + EXAMPLES:: + + sage: A = B = list('abcd') + sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2) + sage: pi = lambda p1, p2: 'abcdefgh'[A.index(p1) + A.index(p2)] + sage: rho = lambda s1, s2: (s1 + s2) % 2 + sage: bij.set_intertwining_relations((2, pi, rho)) + sage: bij._initialize_new_bmilp() + """ preimage_blocks = self._preprocess_intertwining_relations() self._compute_possible_block_values() @@ -2777,6 +2835,26 @@ def _veto_current_solution(self): the set of variables `x_{p, z}` with value 1, that is, the set of variables indicating the current solution. + EXAMPLES:: + + sage: A = B = ["a", "b", "c"] + sage: bij = Bijectionist(A, B, lambda x: A.index(x) % 2) + sage: bij.set_constant_blocks([["a", "b"]]) + sage: iter = bij.solutions_iterator() + sage: next(iter) # indirect doctest + {'a': 0, 'b': 0, 'c': 1} + sage: bij._show_bmilp(bij._bmilp) + Constraints are: + block a: 1 <= x_0 + x_1 <= 1 + block c: 1 <= x_2 + x_3 <= 1 + statistics: 2 <= 2 x_0 + x_2 <= 2 + statistics: 1 <= 2 x_1 + x_3 <= 1 + veto: x_0 + x_3 <= 1 + Variables are: + x_0: s(a) = s(b) = 0 + x_1: s(a) = s(b) = 1 + x_2: s(c) = 0 + x_3: s(c) = 1 """ # get all variables with value 1 active_vars = [self._x[p, z] From d50b62c2008a96849f99004a4c2ad883fab7d0f2 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Fri, 23 Dec 2022 09:59:31 +0100 Subject: [PATCH 17/51] expand docstring of main class --- src/sage/combinat/bijectionist.py | 41 ++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index e1aaf010ea2..c1e55b42852 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -329,10 +329,12 @@ {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 3, [3, 2, 1]: 0} Value restrictions:: + sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] sage: tau = Permutation.longest_increasing_subsequence_length - sage: bij = Bijectionist(A, B, tau, alpha_beta=((len, len),), a_values=((Permutation([1, 2]), [1]), - ....: (Permutation([3, 2, 1]), [2, 3, 4]),)) + sage: alpha_beta = [(len, len)] + sage: value_restrictions = [(Permutation([1, 2]), [1]), (Permutation([3, 2, 1]), [2, 3, 4])] + sage: bij = Bijectionist(A, B, tau, alpha_beta=alpha_beta, value_restrictions=value_restrictions) sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): ....: print(sol) {[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 3} @@ -341,7 +343,7 @@ sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] sage: tau = Permutation.longest_increasing_subsequence_length - sage: bij = Bijectionist(A, B, tau, a_values=((Permutation([1, 2]), [4, 5]),)) + sage: bij = Bijectionist(A, B, tau, value_restrictions=((Permutation([1, 2]), [4, 5]),)) Traceback (most recent call last): ... ValueError: No possible values found for singleton block [[1, 2]] @@ -380,8 +382,8 @@ class Bijectionist(SageObject): r""" - A toolbox to list all possible bijections between two finite sets - under various constraints. + A toolbox to list all possible bijections between two finite + sets under various constraints. INPUT: @@ -391,13 +393,13 @@ class Bijectionist(SageObject): to ``Z``, in case of ``None``, the identity map ``lambda x: x`` is used - - ``alpha`` (optional) -- a statistic from ``A`` to ``W`` - - - ``beta`` (optional) -- a statistic from ``B`` to ``W`` + - ``alpha_beta`` (optional) -- a list of pairs of statistics + ``alpha`` from ``A`` to ``W`` and ``beta`` from ``B`` to ``W`` - ``P`` (optional) -- a partition of ``A`` - - ``pi_rho`` (optional) -- a triple ``(k, pi, rho)`` where + - ``pi_rho`` (optional) -- a list of triples ``(k, pi, rho)`` + where - ``pi`` is a ``k``-ary operation composing objects in ``A`` and @@ -405,6 +407,15 @@ class Bijectionist(SageObject): - ``rho`` is a ``k``-ary function composing statistic values in `Z` + - ``elements_distributions`` (optional) -- a list of pairs ``(tA, + tZ)``, specifying the distributions of ``tA`` + + - ``value_restrictions`` (optional) -- a list of pairs ``(a, + tZ)``, restricting the possible values of ``a`` + + - ``solver`` (optional) -- the backend used to solve the mixed + integer linear programs + ``W`` and ``Z`` can be arbitrary sets. As a natural example we may think of the natural numbers or tuples of integers. @@ -470,7 +481,9 @@ class Bijectionist(SageObject): specification. """ - def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], pi_rho=tuple(), elements_distributions=tuple(), a_values=tuple(), solver=None, key=None): + def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], + pi_rho=tuple(), elements_distributions=tuple(), + value_restrictions=tuple(), solver=None, key=None): """ Initialize the bijectionist. @@ -521,7 +534,7 @@ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], pi_rho=tuple(), ele # set optional inputs self.set_statistics(*alpha_beta) - self.set_value_restrictions(*a_values) + self.set_value_restrictions(*value_restrictions) self.set_distributions(*elements_distributions) self.set_intertwining_relations(*pi_rho) self.set_constant_blocks(P) @@ -940,7 +953,7 @@ def statistics_table(self, header=True): return output_alphas, output_tau_betas - def set_value_restrictions(self, *a_values): + def set_value_restrictions(self, *value_restrictions): r""" Restrict the set of possible values `s(a)` for a given element `a`. @@ -952,7 +965,7 @@ def set_value_restrictions(self, *a_values): INPUT: - - ``a_values`` -- one or more pairs `(a\in A, \tilde + - ``value_restrictions`` -- one or more pairs `(a\in A, \tilde Z\subseteq Z)` EXAMPLES: @@ -1038,7 +1051,7 @@ def set_value_restrictions(self, *a_values): self._bmilp = None set_Z = set(self._Z) self._restrictions_possible_values = {a: set_Z for a in self._A} - for a, values in a_values: + for a, values in value_restrictions: assert a in self._A, f"Element {a} was not found in A" self._restrictions_possible_values[a] = self._restrictions_possible_values[a].intersection(values) From 288e3917aaa8a2d56ec45c2137c5f4889842488e Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Fri, 23 Dec 2022 16:27:43 +0100 Subject: [PATCH 18/51] copy milp instead of adding and removing constraints --- src/sage/combinat/bijectionist.py | 42 +++++++++++-------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index c1e55b42852..2d9b74eda86 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -156,7 +156,7 @@ sage: bij = Bijectionist(A, B) sage: bij.set_intertwining_relations((2, concat_path, concat_tree)) sage: bij.set_statistics((lambda d: d.semilength(), lambda t: t.node_number())) - sage: for D in bij.minimal_subdistributions_iterator(): + sage: for D in sorted(bij.minimal_subdistributions_iterator(), key=lambda x: (len(x[0][0]), x)): ....: ascii_art(D) ( [ /\ ], [ o ] ) ( [ o ] ) @@ -376,9 +376,6 @@ from copy import copy, deepcopy from sage.misc.verbose import get_verbose -# TODO: (low) we frequently need variable names for subsets of A, B, -# Z. In LaTeX, we mostly call them \tilde A, \tilde Z, etc. now. It -# would be good to have a standard name in code, too. class Bijectionist(SageObject): r""" @@ -496,8 +493,8 @@ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], """ # glossary of standard letters: # A, B, Z, W ... finite sets - # ???? tilde_A, tilde_Z, ..., subsets? # P ... set partition of A + # tA, tB, tZ, tP ... subsets # a in A, b in B, p in P # S: A -> B # alpha: A -> W, beta: B -> W @@ -505,7 +502,6 @@ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], # k arity of pi and rho # pi: A^k -> A, rho: Z^k -> Z # a_tuple in A^k - assert len(A) == len(set(A)), "A must have distinct items" assert len(B) == len(set(B)), "B must have distinct items" self._bmilp = None @@ -1230,14 +1226,14 @@ def set_distributions(self, *elements_distributions): """ self._bmilp = None - for elements, values in elements_distributions: - assert len(elements) == len(values), f"{elements} and {values} are not of the same size!" - for a, z in zip(elements, values): + for tA, tZ in elements_distributions: + assert len(tA) == len(tZ), f"{elements} and {values} are not of the same size!" + for a, z in zip(tA, tZ): if a not in self._A: raise ValueError(f"Element {a} was not found in A!") if z not in self._Z: raise ValueError(f"Value {z} was not found in tau(A)!") - self._elements_distributions = elements_distributions + self._elements_distributions = tuple(elements_distributions) def set_intertwining_relations(self, *pi_rho): r""" @@ -1862,7 +1858,6 @@ def _find_counter_example(self, P, s0, d, on_blocks): pass return - def minimal_subdistributions_blocks_iterator(self): r""" Return all representatives of minimal subsets `\tilde P` @@ -2547,7 +2542,7 @@ def _show_bmilp(self, bmilp, variables=True): EXAMPLES:: sage: A = B = ["a", "b", "c"] - sage: bij = Bijectionist(A, B, lambda x: A.index(x) % 2) + sage: bij = Bijectionist(A, B, lambda x: A.index(x) % 2, solver="GLPK") sage: bij.set_constant_blocks([["a", "b"]]) sage: next(bij.solutions_iterator()) {'a': 0, 'b': 0, 'c': 1} @@ -2732,7 +2727,7 @@ def solve(self, additional_constraints, solution_index=0): sage: bmilp.solve([bmilp._x[DyckWord([1,0,1,0]), 1] <= 0.5], solution_index=1) Traceback (most recent call last): ... - MIPSolverException: GLPK: Problem has no feasible solution + MIPSolverException: ... no feasible solution However, searching for a cached solution succeeds, for inequalities and equalities:: @@ -2780,20 +2775,11 @@ def solve(self, additional_constraints, solution_index=0): return self.last_solution # otherwise generate a new one - try: - # TODO: wouldn't it be safer to copy the milp? - n_constraints = self.milp.number_of_constraints() - for constraint in additional_constraints: - self.milp.add_constraint(constraint) - self.milp.solve() - for _ in range(self.milp.number_of_constraints() - n_constraints): - self.milp.remove_constraint(n_constraints) - except MIPSolverException as error: - for _ in range(self.milp.number_of_constraints() - n_constraints): - self.milp.remove_constraint(n_constraints) - raise error - - self.last_solution = self.milp.get_values(self._x) + tmp_milp = deepcopy(self.milp) + for constraint in additional_constraints: + tmp_milp.add_constraint(constraint) + tmp_milp.solve() + self.last_solution = tmp_milp.get_values(self._x.copy_for_mip(tmp_milp)) self._solution_cache.append(self.last_solution) self._veto_current_solution() return self.last_solution @@ -2873,7 +2859,7 @@ def _veto_current_solution(self): active_vars = [self._x[p, z] for p in _disjoint_set_roots(self._bijectionist._P) for z in self._bijectionist._possible_block_values[p] - if self.milp.get_values(self._x[p, z])] + if self.last_solution[(p, z)]] # add constraint that not all of these can be 1, thus vetoing # the current solution From d57c8e5295144152214c870f3a206ab0dc5a7716 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Fri, 23 Dec 2022 17:21:48 +0100 Subject: [PATCH 19/51] derandomize a test, mark example as random --- src/sage/combinat/bijectionist.py | 125 +++++------------------------- 1 file changed, 21 insertions(+), 104 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 2d9b74eda86..cd6fa9b3c94 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -1950,7 +1950,7 @@ def minimal_subdistributions_blocks_iterator(self): [1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1], [1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]] - sage: sorted(bij.minimal_subdistributions_blocks_iterator()) + sage: sorted(bij.minimal_subdistributions_blocks_iterator()) # random [(['a1', 'a2', 'a3', 'a4', 'a5', 'a5', 'a6', 'a6', 'a7', 'a7', 'a8', 'a8'], [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1]), (['a3', 'a4', 'd'], [0, 0, 1]), @@ -2371,113 +2371,30 @@ def solutions_iterator(self): Generate a solution in iterator1, iterator2 should generate the same solution and vice versa:: - sage: next(iterator1) - {'a1': 1, - 'a2': 1, - 'a3': 0, - 'a4': 0, - 'a5': 0, - 'a6': 1, - 'a7': 0, - 'a8': 0, - 'b3': 0, - 'b4': 0, - 'b5': 0, - 'b6': 1, - 'b7': 0, - 'b8': 0, - 'd': 1} - - sage: next(iterator2) - {'a1': 1, - 'a2': 1, - 'a3': 0, - 'a4': 0, - 'a5': 0, - 'a6': 1, - 'a7': 0, - 'a8': 0, - 'b3': 0, - 'b4': 0, - 'b5': 0, - 'b6': 1, - 'b7': 0, - 'b8': 0, - 'd': 1} - - sage: next(iterator2) - {'a1': 1, - 'a2': 1, - 'a3': 0, - 'a4': 0, - 'a5': 1, - 'a6': 0, - 'a7': 0, - 'a8': 0, - 'b3': 0, - 'b4': 0, - 'b5': 1, - 'b6': 0, - 'b7': 0, - 'b8': 0, - 'd': 1} - - sage: next(iterator1) - {'a1': 1, - 'a2': 1, - 'a3': 0, - 'a4': 0, - 'a5': 1, - 'a6': 0, - 'a7': 0, - 'a8': 0, - 'b3': 0, - 'b4': 0, - 'b5': 1, - 'b6': 0, - 'b7': 0, - 'b8': 0, - 'd': 1} - - Re-setting the distribution resets the cache, so a new iterator will generate the first solutions again, - but the old iterator continues:: + sage: s1_1 = next(iterator1) + sage: s2_1 = next(iterator2) + sage: s1_1 == s2_1 + True + sage: s2_2 = next(iterator2) + sage: s1_2 = next(iterator1) + sage: s1_2 == s2_2 + True + + Re-setting the distribution resets the cache, so a new + iterator will generate the first solutions again, but the old + iterator continues:: sage: bij.set_distributions((A[:8] + A[8+2:-1], d), (A[:8] + A[8:-3], d)) sage: iterator3 = bij.solutions_iterator() - sage: next(iterator3) - {'a1': 1, - 'a2': 1, - 'a3': 0, - 'a4': 0, - 'a5': 0, - 'a6': 1, - 'a7': 0, - 'a8': 0, - 'b3': 0, - 'b4': 0, - 'b5': 0, - 'b6': 1, - 'b7': 0, - 'b8': 0, - 'd': 1} - - sage: next(iterator1) - {'a1': 0, - 'a2': 1, - 'a3': 0, - 'a4': 1, - 'a5': 0, - 'a6': 0, - 'a7': 0, - 'a8': 1, - 'b3': 0, - 'b4': 1, - 'b5': 0, - 'b6': 0, - 'b7': 0, - 'b8': 1, - 'd': 0} + sage: s3_1 = next(iterator3) + sage: s1_1 == s3_1 + True + + sage: s1_3 = next(iterator1) + sage: len(set([tuple(sorted(s.items())) for s in [s1_1, s1_2, s1_3]])) + 3 + """ next_solution = None if self._bmilp is None: From 7353fb523075c798dd3afe2cd57108b88909c94a Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Fri, 23 Dec 2022 22:40:49 +0100 Subject: [PATCH 20/51] correct typo, remove useless assignment --- src/sage/combinat/bijectionist.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index cd6fa9b3c94..b9b1bf7e98f 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -1227,7 +1227,7 @@ def set_distributions(self, *elements_distributions): """ self._bmilp = None for tA, tZ in elements_distributions: - assert len(tA) == len(tZ), f"{elements} and {values} are not of the same size!" + assert len(tA) == len(tZ), f"{tA} and {tZ} are not of the same size!" for a, z in zip(tA, tZ): if a not in self._A: raise ValueError(f"Element {a} was not found in A!") @@ -2396,7 +2396,6 @@ def solutions_iterator(self): 3 """ - next_solution = None if self._bmilp is None: try: self._bmilp = self._initialize_new_bmilp() From db850f00bdcea2011ed9194de177fbcbcc8b93d1 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Fri, 23 Dec 2022 23:35:42 +0100 Subject: [PATCH 21/51] add and remove constraints instead of copying the whole program --- src/sage/combinat/bijectionist.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index b9b1bf7e98f..26418062876 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -2691,10 +2691,26 @@ def solve(self, additional_constraints, solution_index=0): return self.last_solution # otherwise generate a new one - tmp_milp = deepcopy(self.milp) + # set copy = True if the solver requires us to copy the program and throw it away again + copy = False + if copy: + tmp_milp = deepcopy(self.milp) + else: + tmp_milp = self.milp + for constraint in additional_constraints: tmp_milp.add_constraint(constraint) - tmp_milp.solve() + + if copy: + tmp_milp.solve() + else: + n = tmp_milp.number_of_constraints() - 1 + try: + tmp_milp.solve() + finally: + for i in range(len(additional_constraints)): + tmp_milp.remove_constraint(n - i) + self.last_solution = tmp_milp.get_values(self._x.copy_for_mip(tmp_milp)) self._solution_cache.append(self.last_solution) self._veto_current_solution() From ec3271c0e0d79727e2f5f80867bcb253eb6a639e Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Sat, 24 Dec 2022 00:00:37 +0100 Subject: [PATCH 22/51] fix problem with SCIP, add timings --- src/sage/combinat/bijectionist.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 26418062876..4bcd34fda8d 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -2707,11 +2707,11 @@ def solve(self, additional_constraints, solution_index=0): n = tmp_milp.number_of_constraints() - 1 try: tmp_milp.solve() + self.last_solution = tmp_milp.get_values(self._x.copy_for_mip(tmp_milp)) finally: for i in range(len(additional_constraints)): tmp_milp.remove_constraint(n - i) - self.last_solution = tmp_milp.get_values(self._x.copy_for_mip(tmp_milp)) self._solution_cache.append(self.last_solution) self._veto_current_solution() return self.last_solution @@ -3100,7 +3100,7 @@ def _non_copying_intersection(sets): sage: bij = Bijectionist(sum(As, []), sum(Bs, [])) sage: bij.set_statistics((lambda x: x[0], lambda x: x[0])) sage: bij.set_intertwining_relations((2, c1, c1), (1, c2, c2)) - sage: l = list(bij.solutions_iterator()); len(l) # long time + sage: l = list(bij.solutions_iterator()); len(l) # long time -- (2.7 seconds with SCIP on AMD Ryzen 5 PRO 3500U w/ Radeon Vega Mobile Gfx) 64 A brute force check would be difficult:: @@ -3128,7 +3128,7 @@ def _non_copying_intersection(sets): sage: A = sum(As, []) sage: respects_c1 = lambda s: all(c1(a1, a2) not in A or s[c1(a1, a2)] == c1(s[a1], s[a2]) for a1 in A for a2 in A) sage: respects_c2 = lambda s: all(c2(a1) not in A or s[c2(a1)] == c2(s[a1]) for a1 in A) - sage: l2 = [s for s in it if respects_c1(s) and respects_c2(s)] # long time + sage: l2 = [s for s in it if respects_c1(s) and respects_c2(s)] # long time -- (17 seconds with SCIP on AMD Ryzen 5 PRO 3500U w/ Radeon Vega Mobile Gfx) sage: sorted(l1, key=lambda s: tuple(s.items())) == l2 # long time True @@ -3176,7 +3176,7 @@ def _non_copying_intersection(sets): ([[2, 1, 5, 3, 4], [2, 5, 1, 3, 4], [3, 1, 5, 2, 4], [3, 5, 1, 2, 4]], [3, 3, 4, 4]) ([[1, 3, 2, 5, 4], [1, 3, 5, 2, 4], [1, 4, 2, 5, 3], [1, 4, 5, 2, 3], [1, 4, 5, 3, 2], [1, 5, 4, 2, 3], [1, 5, 4, 3, 2]], [2, 2, 3, 3, 3, 3, 3]) - sage: l = list(bij.solutions_iterator()); len(l) # not tested + sage: l = list(bij.solutions_iterator()); len(l) # not tested -- (17 seconds with SCIP on AMD Ryzen 5 PRO 3500U w/ Radeon Vega Mobile Gfx) 504 sage: for a, d in bij.minimal_subdistributions_iterator(): # not tested From dfdc6bacdcdbc2198fd62b96167aedc3bf94e1de Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Sat, 24 Dec 2022 14:58:35 +0100 Subject: [PATCH 23/51] use convert for MixedIntegerLinearProgram.get_values --- src/sage/combinat/bijectionist.py | 51 ++++++++++++++++--------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 4bcd34fda8d..e33c6602f06 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -1785,7 +1785,7 @@ def minimal_subdistributions_iterator(self): minimal_subdistribution.solve() except MIPSolverException: return - d = minimal_subdistribution.get_values(D) # a dict from A to {0, 1} + d = minimal_subdistribution.get_values(D, convert=bool, tolerance=0.1) # a dict from A to {0, 1} new_s = self._find_counter_example(self._A, s, d, False) if new_s is None: values = self._sorter["Z"](s[a] for a in self._A if d[a]) @@ -1793,7 +1793,7 @@ def minimal_subdistributions_iterator(self): # get all variables with value 1 active_vars = [D[a] for a in self._A - if minimal_subdistribution.get_values(D[a])] + if minimal_subdistribution.get_values(D[a], convert=bool, tolerance=0.1)] # add constraint that not all of these can be 1, thus vetoing # the current solution @@ -2006,7 +2006,7 @@ def add_counter_example_constraint(s): minimal_subdistribution.solve() except MIPSolverException: return - d = minimal_subdistribution.get_values(D) # a dict from P to multiplicities + d = minimal_subdistribution.get_values(D, convert=ZZ, tolerance=0.1) # a dict from P to multiplicities new_s = self._find_counter_example(P, s, d, True) if new_s is None: yield ([p for p in P for _ in range(ZZ(d[p]))], @@ -2612,11 +2612,11 @@ def solve(self, additional_constraints, solution_index=0): Without any constraints, we do not require that the solution is a bijection:: sage: bmilp.solve([bmilp._x["a", "a"] == 1, bmilp._x["b", "a"] == 1]) - {('a', 'a'): 1.0, ('a', 'b'): 0.0, ('b', 'a'): 1.0, ('b', 'b'): 0.0} + {('a', 'a'): True, ('a', 'b'): False, ('b', 'a'): True, ('b', 'b'): False} sage: len(bmilp._solution_cache) 1 sage: bmilp.solve([bmilp._x["a", "b"] == 1, bmilp._x["b", "b"] == 1]) - {('a', 'a'): 0.0, ('a', 'b'): 1.0, ('b', 'a'): 0.0, ('b', 'b'): 1.0} + {('a', 'a'): False, ('a', 'b'): True, ('b', 'a'): False, ('b', 'b'): True} sage: len(bmilp._solution_cache) 2 @@ -2631,12 +2631,12 @@ def solve(self, additional_constraints, solution_index=0): Generate a solution:: sage: bmilp.solve([]) - {([], 0): 1.0, - ([1, 0], 1): 1.0, - ([1, 0, 1, 0], 1): 0.0, - ([1, 0, 1, 0], 2): 1.0, - ([1, 1, 0, 0], 1): 1.0, - ([1, 1, 0, 0], 2): 0.0} + {([], 0): True, + ([1, 0], 1): True, + ([1, 0, 1, 0], 1): False, + ([1, 0, 1, 0], 2): True, + ([1, 1, 0, 0], 1): True, + ([1, 1, 0, 0], 2): False} Generating a new solution that also maps `1010` to `2` fails: @@ -2648,20 +2648,20 @@ def solve(self, additional_constraints, solution_index=0): However, searching for a cached solution succeeds, for inequalities and equalities:: sage: bmilp.solve([bmilp._x[DyckWord([1,0,1,0]), 1] <= 0.5]) - {([], 0): 1.0, - ([1, 0], 1): 1.0, - ([1, 0, 1, 0], 1): 0.0, - ([1, 0, 1, 0], 2): 1.0, - ([1, 1, 0, 0], 1): 1.0, - ([1, 1, 0, 0], 2): 0.0} + {([], 0): True, + ([1, 0], 1): True, + ([1, 0, 1, 0], 1): False, + ([1, 0, 1, 0], 2): True, + ([1, 1, 0, 0], 1): True, + ([1, 1, 0, 0], 2): False} sage: bmilp.solve([bmilp._x[DyckWord([1,0,1,0]), 1] == 0]) - {([], 0): 1.0, - ([1, 0], 1): 1.0, - ([1, 0, 1, 0], 1): 0.0, - ([1, 0, 1, 0], 2): 1.0, - ([1, 1, 0, 0], 1): 1.0, - ([1, 1, 0, 0], 2): 0.0} + {([], 0): True, + ([1, 0], 1): True, + ([1, 0, 1, 0], 1): False, + ([1, 0, 1, 0], 2): True, + ([1, 1, 0, 0], 1): True, + ([1, 1, 0, 0], 2): False} """ assert 0 <= solution_index <= len(self._solution_cache), "the index of the desired solution must not be larger than the number of known solutions" @@ -2707,7 +2707,8 @@ def solve(self, additional_constraints, solution_index=0): n = tmp_milp.number_of_constraints() - 1 try: tmp_milp.solve() - self.last_solution = tmp_milp.get_values(self._x.copy_for_mip(tmp_milp)) + self.last_solution = tmp_milp.get_values(self._x.copy_for_mip(tmp_milp), + convert=bool, tolerance=0.1) finally: for i in range(len(additional_constraints)): tmp_milp.remove_constraint(n - i) @@ -2844,7 +2845,7 @@ def add_alpha_beta_constraints(self): sage: bmilp = _BijectionistMILP(bij) sage: bmilp.add_alpha_beta_constraints() sage: bmilp.solve([]) - {([], 0): 1.0, ([1], 1): 1.0, ([1, 2], 2): 1.0, ([2, 1], 2): 1.0} + {([], 0): True, ([1], 1): True, ([1, 2], 2): True, ([2, 1], 2): True} """ W = self._bijectionist._W Z = self._bijectionist._Z From d0836cb93095985a933c549ea8a8893ba4d94a04 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Sun, 25 Dec 2022 22:26:12 +0100 Subject: [PATCH 24/51] include information on MILP also in public documentation --- src/sage/combinat/bijectionist.py | 81 ++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index e33c6602f06..7c4cb15b274 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -660,6 +660,17 @@ def set_statistics(self, *alpha_beta): If the statistics `\alpha` and `\beta` are not equidistributed, an error is raised. + ALGORITHM: + + We add + + .. MATH:: + + \sum_{a\in A, z\in Z} x_{p(a), z} s^z t^{\alpha(a)} + = \sum_{b\in B} s^{\tau(b)} t(\beta(b)) + + as a matrix equation to the MILP. + EXAMPLES: We look for bijections `S` on permutations such that the @@ -1094,16 +1105,28 @@ def set_distributions(self, *elements_distributions): INPUT: - - one or more pairs of `(\tilde A\subseteq A, \tilde Z)`, - where `\tilde Z` is a list of values in `Z` of the same - size as `\tilde A` + - one or more pairs of `(\tilde A, \tilde Z)`, where `\tilde + A\subseteq A` and `\tilde Z` is a list of values in `Z` of + the same size as `\tilde A` This method specifies that `\{s(a) | a\in\tilde A\}` equals - ``\tilde Z`` as a multiset for each of the pairs. + `\tilde Z` as a multiset for each of the pairs. When specifying several distributions, the subsets of `A` do not have to be disjoint. + ALGORITHM: + + We add + + .. MATH:: + + \sum_{a\in\tilde A} x_{p(a), z}t^z = \sum_{z\in\tilde Z} t^z, + + where `p(a)` is the block containing `a`, for each given + distribution as a vector equation to the MILP. + + EXAMPLES:: sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] @@ -1248,13 +1271,45 @@ def set_intertwining_relations(self, *pi_rho): INPUT: - ``pi_rho`` -- one or more tuples `(k, \pi: A^k\to A, \rho: - Z^k\to Z, \tilde A)` where `\tilde A` (optional) is an - `k`-ary function that returns true if and only if an + Z^k\to Z, \tilde A)` where `\tilde A` (optional) is a + `k`-ary function that returns true if and only if a `k`-tuple of objects in `A` is in the domain of `\pi` + ALGORITHM: + + The relation + + .. MATH:: + + s(\pi(a_1,\dots, a_k)) = \rho(s(a_1),\dots, s(a_k)) + + for each pair `(\pi, \rho)` implies immediately that + `s(\pi(a_1,\dots, a_k))` only depends on the blocks of + `a_1,\dots, a_k`. + + The MILP formulation is as follows. Let `a_1,\dots,a_k \in + A` and let `a = \pi(a_1,\dots,a_k)`. Let `z_1,\dots,z_k \in + Z` and let `z = \rho(z_1,\dots,z_k)`. Suppose that `a_i\in + p_i` for all `i` and that `a\in p`. + + We then want to model the implication + + .. MATH:: + + x_{p_1, z_1} = 1,\dots, x_{p_k, z_k} = 1 \Rightarrow x_{p, z} = 1. + + We achieve this by requiring + + .. MATH:: + + x_{p, z}\geq 1 - k + \sum_{i=1}^k x_{p_i, z_i}. + + Note that `z` must be a possible value of `p` and each `z_i` + must be a possible value of `p_i`. + EXAMPLES: - We can concatenate two permutations, by increasing the values + We can concatenate two permutations by increasing the values of the second permutation by the length of the first permutation:: @@ -2184,10 +2239,10 @@ def solutions_iterator(self): x_{p, z} \geq 1-k + \sum_{i=1}^k x_{p_i, z_i}. * for each distribution restriction, i.e. a set of elements - `e` and a distribution of values given by integers `d_z` - representing the multiplicity of each `z \in Z`, and `r_p = - |p \cap e|` indicating the relative size of block `p` in - the set of elements of the distribution, + `\tilde A` and a distribution of values given by integers + `d_z` representing the multiplicity of each `z \in Z`, and + `r_p = |p \cap\tilde A|` indicating the relative size of + block `p` in the set of elements of the distribution, .. MATH:: @@ -2887,7 +2942,7 @@ def add_distribution_constraints(self): .. MATH:: - \sum_{a\in elements} x_{p(a), z}t^z = \sum_{z\in values} t^z, + \sum_{a\in\tilde A} x_{p(a), z}t^z = \sum_{z\in\tilde Z} t^z, where `p(a)` is the block containing `a`, for each given distribution as a vector equation. @@ -2950,7 +3005,7 @@ def add_intertwining_relation_constraints(self, origins): .. MATH:: - s(\pi(a_1,\dots, a_k)) = \rho(s(a_1),\dots, s(a_k))` + s(\pi(a_1,\dots, a_k)) = \rho(s(a_1),\dots, s(a_k)) for each pair `(\pi, \rho)`. The relation implies immediately that `s(\pi(a_1,\dots, a_k))` only depends on the From 563af988ae4e282cc2778599c73b728e8eb71f0c Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Sun, 25 Dec 2022 23:25:51 +0100 Subject: [PATCH 25/51] remove unnecessary copy --- src/sage/combinat/bijectionist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 7c4cb15b274..623b81d4753 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -2762,7 +2762,7 @@ def solve(self, additional_constraints, solution_index=0): n = tmp_milp.number_of_constraints() - 1 try: tmp_milp.solve() - self.last_solution = tmp_milp.get_values(self._x.copy_for_mip(tmp_milp), + self.last_solution = tmp_milp.get_values(self._x, convert=bool, tolerance=0.1) finally: for i in range(len(additional_constraints)): From 590bae24937ea07a1d5f22216b0900cce949ce55 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Mon, 26 Dec 2022 19:36:19 +0100 Subject: [PATCH 26/51] add possibility to constrain to involutions --- src/sage/combinat/bijectionist.py | 158 +++++++++++++++++++++++++----- 1 file changed, 136 insertions(+), 22 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 623b81d4753..726185d9907 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -479,7 +479,8 @@ class Bijectionist(SageObject): """ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], - pi_rho=tuple(), elements_distributions=tuple(), + pi_rho=tuple(), phi_psi=tuple(), + elements_distributions=tuple(), value_restrictions=tuple(), solver=None, key=None): """ Initialize the bijectionist. @@ -532,6 +533,7 @@ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], self.set_statistics(*alpha_beta) self.set_value_restrictions(*value_restrictions) self.set_distributions(*elements_distributions) + self.set_pseudo_inverse_relation(*phi_psi) self.set_intertwining_relations(*pi_rho) self.set_constant_blocks(P) @@ -1126,7 +1128,6 @@ def set_distributions(self, *elements_distributions): where `p(a)` is the block containing `a`, for each given distribution as a vector equation to the MILP. - EXAMPLES:: sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] @@ -1389,6 +1390,61 @@ def set_intertwining_relations(self, *pi_rho): self._pi_rho.append(Pi_Rho(numargs=k, pi=pi, rho=rho, domain=domain)) + set_semi_conjugacy = set_intertwining_relations + + def set_pseudo_inverse_relation(self, *phi_psi): + r""" + Add restrictions of the form `s\circ\psi\circ s = \phi`. + + INPUT: + + - ``phi_psi`` (optional) -- a list of pairs `(\phi, \rho)` + where `\phi: A\to Z` and `\psi: Z\to A` + + ALGORITHM: + + We add + + .. MATH:: + + x_{p(a), z} = x_{p(\psi(z)), \phi(a)} + + for `a\in A` and `z\in Z` to the MILP, where `\phi:A\to Z` + and `\psi:Z\to A`. Note that, in particular, `\phi` must be + constant on blocks. + + + EXAMPLES:: + + sage: A = B = DyckWords(3) + sage: bij = Bijectionist(A, B) + sage: bij.set_statistics((lambda D: D.number_of_touch_points(), lambda D: D.number_of_initial_rises())) + sage: ascii_art(list(bij.minimal_subdistributions_iterator())) + [ ( [ /\ ] ) + [ ( [ / \ ] ) ( [ /\ /\ ] [ /\ /\/\ ] ) + [ ( [ /\/\/\ ], [ / \ ] ), ( [ /\/ \, / \/\ ], [ / \/\, / \ ] ), + + ( [ /\ ] ) ] + ( [ /\/\ / \ ] [ /\ ] ) ] + ( [ / \, / \ ], [ /\/\/\, /\/ \ ] ) ] + sage: bij.set_pseudo_inverse_relation((lambda D: D, lambda D: D)) + sage: ascii_art(list(bij.minimal_subdistributions_iterator())) + [ ( [ /\ ] ) + [ ( [ / \ ] ) ( [ /\ ] [ /\/\ ] ) + [ ( [ /\/\/\ ], [ / \ ] ), ( [ /\/ \ ], [ / \ ] ), + + + ( [ /\ ] [ /\ ] ) ( [ /\/\ ] [ /\ ] ) + ( [ / \/\ ], [ / \/\ ] ), ( [ / \ ], [ /\/ \ ] ), + + ( [ /\ ] ) ] + ( [ / \ ] ) ] + ( [ / \ ], [ /\/\/\ ] ) ] + + """ + self._bmilp = None + self._phi_psi = phi_psi + def _forced_constant_blocks(self): r""" Modify current partition into blocks to the coarsest possible @@ -1717,7 +1773,7 @@ def possible_values(self, p=None, optimal=False): blocks = set() if p in self._A: blocks.add(self._P.find(p)) - elif type(p) is list: + elif type(p) is list: # TODO: this looks very brittle for p1 in p: if p1 in self._A: blocks.add(self._P.find(p1)) @@ -1890,22 +1946,22 @@ def _find_counter_example(self, P, s0, d, on_blocks): {'a': 2, 'b': 2, 'c': 1, 'd': 3, 'e': 1} """ bmilp = self._bmilp - for v in self._Z: - v_in_d_count = sum(d[p] for p in P if s0[p] == v) - if not v_in_d_count: + for z in self._Z: + z_in_d_count = sum(d[p] for p in P if s0[p] == z) + if not z_in_d_count: continue # try to find a solution which has a different # subdistribution on d than s0 - v_in_d = sum(d[p] * bmilp._x[self._P.find(p), v] + z_in_d = sum(d[p] * bmilp._x[self._P.find(p), z] for p in P - if v in self._possible_block_values[self._P.find(p)]) + if z in self._possible_block_values[self._P.find(p)]) - # it is sufficient to require that v occurs less often as + # it is sufficient to require that z occurs less often as # a value among {a | d[a] == 1} than it does in - # v_in_d_count, because, if the distributions are - # different, one such v must exist - tmp_constraints = [v_in_d <= v_in_d_count - 1] + # z_in_d_count, because, if the distributions are + # different, one such z must exist + tmp_constraints = [z_in_d <= z_in_d_count - 1] try: bmilp.solve(tmp_constraints) return self._solution(bmilp, on_blocks) @@ -2587,6 +2643,7 @@ def _initialize_new_bmilp(self): n = bmilp.milp.number_of_variables() bmilp.add_alpha_beta_constraints() bmilp.add_distribution_constraints() + bmilp.add_pseudo_inverse_relation_constraints() bmilp.add_intertwining_relation_constraints(preimage_blocks) if get_verbose() >= 2: self._show_bmilp(bmilp) @@ -2969,25 +3026,25 @@ def add_distribution_constraints(self): """ Z = self._bijectionist._Z Z_dict = {z: i for i, z in enumerate(Z)} - for elements, values in self._bijectionist._elements_distributions: - elements_sum = [ZZ(0)]*len(Z_dict) - values_sum = [ZZ(0)]*len(Z_dict) - for a in elements: + for tA, tZ in self._bijectionist._elements_distributions: + tA_sum = [ZZ(0)]*len(Z_dict) + tZ_sum = [ZZ(0)]*len(Z_dict) + for a in tA: p = self._bijectionist._P.find(a) for z in self._bijectionist._possible_block_values[p]: - elements_sum[Z_dict[z]] += self._x[p, z] - for z in values: - values_sum[Z_dict[z]] += 1 + tA_sum[Z_dict[z]] += self._x[p, z] + for z in tZ: + tZ_sum[Z_dict[z]] += 1 # TODO: not sure that this is the best way to filter out # empty conditions - for element, value in zip(elements_sum, values_sum): - c = element - value + for a, z in zip(tA_sum, tZ_sum): + c = a - z if c.is_zero(): continue if c in ZZ: raise MIPSolverException - self.milp.add_constraint(c == 0, name=f"d: {element} == {value}") + self.milp.add_constraint(c == 0, name=f"d: {a} == {z}") def add_intertwining_relation_constraints(self, origins): r""" @@ -3065,6 +3122,63 @@ def add_intertwining_relation_constraints(self, origins): self.milp.add_constraint(rhs <= 0, name=f"pi/rho({composition_index})") + def add_pseudo_inverse_relation_constraints(self): + r""" + Add constraints enforcing that `s\circ\phi\circ s = + \psi`. + + We do this by adding + + .. MATH:: + + x_{p(a), z} = x_{p(\psi(z)), \phi(a)} + + for `a\in A` and `z\in Z`, where `\phi:A\to Z` and `\psi:Z\to + A`. Note that, in particular, `\phi` must be constant on + blocks. + + EXAMPLES:: + + sage: A = B = DyckWords(3) + sage: bij = Bijectionist(A, B) + sage: bij.set_statistics((lambda D: D.number_of_touch_points(), lambda D: D.number_of_initial_rises())) + sage: ascii_art(list(bij.minimal_subdistributions_iterator())) + [ ( [ /\ ] ) + [ ( [ / \ ] ) ( [ /\ /\ ] [ /\ /\/\ ] ) + [ ( [ /\/\/\ ], [ / \ ] ), ( [ /\/ \, / \/\ ], [ / \/\, / \ ] ), + + ( [ /\ ] ) ] + ( [ /\/\ / \ ] [ /\ ] ) ] + ( [ / \, / \ ], [ /\/\/\, /\/ \ ] ) ] + sage: bij.set_pseudo_inverse_relation((lambda D: D, lambda D: D)) + sage: ascii_art(list(bij.minimal_subdistributions_iterator())) + [ ( [ /\ ] ) + [ ( [ / \ ] ) ( [ /\ ] [ /\/\ ] ) + [ ( [ /\/\/\ ], [ / \ ] ), ( [ /\/ \ ], [ / \ ] ), + + + ( [ /\ ] [ /\ ] ) ( [ /\/\ ] [ /\ ] ) + ( [ / \/\ ], [ / \/\ ] ), ( [ / \ ], [ /\/ \ ] ), + + ( [ /\ ] ) ] + ( [ / \ ] ) ] + ( [ / \ ], [ /\/\/\ ] ) ] + + """ + P = self._bijectionist._P + for phi, psi in self._bijectionist._phi_psi: + for p, block in P.root_to_elements_dict().items(): + z0 = phi(p) + assert all(phi(a) == z0 for a in block), "phi must be constant on the block %s" % block + for z in self._bijectionist._possible_block_values[p]: + p0 = P.find(psi(z)) + if z0 in self._bijectionist._possible_block_values[p0]: + c = self._x[p, z] - self._x[p0, z0] + if c.is_zero(): + continue + self.milp.add_constraint(c == 0, name=f"i: s({p})={z}<->s(psi({z})=phi({p})") + else: + self.milp.add_constraint(self._x[p, z] == 0, name=f"i: s({p})!={z}") def _invert_dict(d): """ From da0655b5e1dcd4b1b1a35943fbbedf2698900786 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Mon, 26 Dec 2022 23:27:45 +0100 Subject: [PATCH 27/51] correct indentation --- src/sage/combinat/bijectionist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 726185d9907..97319f78d5e 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -1399,7 +1399,7 @@ def set_pseudo_inverse_relation(self, *phi_psi): INPUT: - ``phi_psi`` (optional) -- a list of pairs `(\phi, \rho)` - where `\phi: A\to Z` and `\psi: Z\to A` + where `\phi: A\to Z` and `\psi: Z\to A` ALGORITHM: @@ -3150,7 +3150,7 @@ def add_pseudo_inverse_relation_constraints(self): ( [ /\ ] ) ] ( [ /\/\ / \ ] [ /\ ] ) ] ( [ / \, / \ ], [ /\/\/\, /\/ \ ] ) ] - sage: bij.set_pseudo_inverse_relation((lambda D: D, lambda D: D)) + sage: bij.set_pseudo_inverse_relation((lambda D: D, lambda D: D)) # indirect doctest sage: ascii_art(list(bij.minimal_subdistributions_iterator())) [ ( [ /\ ] ) [ ( [ / \ ] ) ( [ /\ ] [ /\/\ ] ) From 03b1fb836611da4674cb8c34ea3faad709418670 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Thu, 29 Dec 2022 13:28:14 +0100 Subject: [PATCH 28/51] better handling of empty constraints --- src/sage/combinat/bijectionist.py | 119 +++++++++++++++--------------- 1 file changed, 59 insertions(+), 60 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 97319f78d5e..980668f7c7b 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -2358,9 +2358,14 @@ def solutions_iterator(self): block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1 block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1 statistics: 1 <= x_0 <= 1 + statistics: 0 <= <= 0 + statistics: 0 <= <= 0 statistics: 1 <= x_1 <= 1 + statistics: 0 <= <= 0 + statistics: 0 <= <= 0 statistics: 1 <= x_2 + x_4 <= 1 statistics: 1 <= x_3 + x_5 <= 1 + statistics: 0 <= <= 0 statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1 statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3 statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2 @@ -2376,9 +2381,14 @@ def solutions_iterator(self): block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1 block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1 statistics: 1 <= x_0 <= 1 + statistics: 0 <= <= 0 + statistics: 0 <= <= 0 statistics: 1 <= x_1 <= 1 + statistics: 0 <= <= 0 + statistics: 0 <= <= 0 statistics: 1 <= x_2 + x_4 <= 1 statistics: 1 <= x_3 + x_5 <= 1 + statistics: 0 <= <= 0 statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1 statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3 statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2 @@ -2399,9 +2409,14 @@ def solutions_iterator(self): block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1 block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1 statistics: 1 <= x_0 <= 1 + statistics: 0 <= <= 0 + statistics: 0 <= <= 0 statistics: 1 <= x_1 <= 1 + statistics: 0 <= <= 0 + statistics: 0 <= <= 0 statistics: 1 <= x_2 + x_4 <= 1 statistics: 1 <= x_3 + x_5 <= 1 + statistics: 0 <= <= 0 statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1 statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3 statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2 @@ -2431,9 +2446,14 @@ def solutions_iterator(self): block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1 block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1 statistics: 1 <= x_0 <= 1 + statistics: 0 <= <= 0 + statistics: 0 <= <= 0 statistics: 1 <= x_1 <= 1 + statistics: 0 <= <= 0 + statistics: 0 <= <= 0 statistics: 1 <= x_2 + x_4 <= 1 statistics: 1 <= x_3 + x_5 <= 1 + statistics: 0 <= <= 0 statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1 statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3 statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2 @@ -2448,9 +2468,14 @@ def solutions_iterator(self): block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1 block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1 statistics: 1 <= x_0 <= 1 + statistics: 0 <= <= 0 + statistics: 0 <= <= 0 statistics: 1 <= x_1 <= 1 + statistics: 0 <= <= 0 + statistics: 0 <= <= 0 statistics: 1 <= x_2 + x_4 <= 1 statistics: 1 <= x_3 + x_5 <= 1 + statistics: 0 <= <= 0 statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1 statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3 statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2 @@ -2640,14 +2665,12 @@ def _initialize_new_bmilp(self): self._compute_possible_block_values() bmilp = _BijectionistMILP(self) - n = bmilp.milp.number_of_variables() bmilp.add_alpha_beta_constraints() bmilp.add_distribution_constraints() bmilp.add_pseudo_inverse_relation_constraints() bmilp.add_intertwining_relation_constraints(preimage_blocks) if get_verbose() >= 2: self._show_bmilp(bmilp) - assert n == bmilp.milp.number_of_variables(), "The number of variables increased." return bmilp @@ -2682,22 +2705,21 @@ def __init__(self, bijectionist: Bijectionist): # the attributes of the bijectionist class we actually use: # _possible_block_values # _elements_distributions - # _W, _Z, _A, _B, _P, _alpha, _beta, _tau, _pi_rho + # _W, _Z, _A, _B, _P, _alpha, _beta, _tau, _pi_rho, _phi_psi + self._bijectionist = bijectionist self.milp = MixedIntegerLinearProgram(solver=bijectionist._solver) self.milp.set_objective(None) - self._n_variables = -1 + indices = [(p, z) + for p, tZ in bijectionist._possible_block_values.items() + for z in tZ] + self._x = self.milp.new_variable(binary=True, indices=indices) self._solution_cache = [] self._last_solution = {} - self._index_block_value_dict = None - self._x = self.milp.new_variable(binary=True) # indexed by P x Z - - self._bijectionist = bijectionist for p in _disjoint_set_roots(bijectionist._P): - name = f"block {p}" self.milp.add_constraint(sum(self._x[p, z] for z in bijectionist._possible_block_values[p]) == 1, - name=name[:50]) + name=f"block {p}"[:50]) def solve(self, additional_constraints, solution_index=0): r""" @@ -2777,20 +2799,8 @@ def solve(self, additional_constraints, solution_index=0): """ assert 0 <= solution_index <= len(self._solution_cache), "the index of the desired solution must not be larger than the number of known solutions" - - if self._n_variables < 0: - # initialize at first call - self._n_variables = self.milp.number_of_variables() - self._index_block_value_dict = {} - for (p, z), v in self._x.items(): - variable_index = next(iter(v.dict().keys())) - self._index_block_value_dict[variable_index] = (p, z) - # number of variables would change with creation of - # constraints with new variables - assert self._n_variables == self.milp.number_of_variables(), "The number of variables changed." - - # check if there is a solution satisfying the constraints in - # the cache + # check whether there is a solution in the cache satisfying + # the additional constraints for solution in self._solution_cache[solution_index:]: if all(all(self._evaluate_linear_function(linear_function, solution) == value.dict()[-1] @@ -2807,23 +2817,19 @@ def solve(self, additional_constraints, solution_index=0): copy = False if copy: tmp_milp = deepcopy(self.milp) - else: - tmp_milp = self.milp - - for constraint in additional_constraints: - tmp_milp.add_constraint(constraint) - - if copy: + for constraint in additional_constraints: + tmp_milp.add_constraint(constraint, return_indices=True) tmp_milp.solve() else: - n = tmp_milp.number_of_constraints() - 1 + new_indices = [] + for constraint in additional_constraints: + new_indices.extend(self.milp.add_constraint(constraint, return_indices=True)) try: - tmp_milp.solve() - self.last_solution = tmp_milp.get_values(self._x, - convert=bool, tolerance=0.1) + self.milp.solve() + self.last_solution = self.milp.get_values(self._x, + convert=bool, tolerance=0.1) finally: - for i in range(len(additional_constraints)): - tmp_milp.remove_constraint(n - i) + self.milp.remove_constraints(new_indices) self._solution_cache.append(self.last_solution) self._veto_current_solution() @@ -2849,13 +2855,16 @@ def _evaluate_linear_function(self, linear_function, values): sage: from sage.combinat.bijectionist import _BijectionistMILP sage: bmilp = _BijectionistMILP(bij) sage: _ = bmilp.solve([]) - sage: bmilp._index_block_value_dict # random - {0: ('a', 'a'), 1: ('a', 'b'), 2: ('b', 'a'), 3: ('b', 'b')} sage: f = bmilp._x["a", "a"] + bmilp._x["b", "a"] sage: v = {('a', 'a'): 1.0, ('a', 'b'): 0.0, ('b', 'a'): 1.0, ('b', 'b'): 0.0} sage: bmilp._evaluate_linear_function(f, v) 2.0 """ + self._index_block_value_dict = {} + for (p, z), v in self._x.items(): + variable_index = next(iter(v.dict().keys())) + self._index_block_value_dict[variable_index] = (p, z) + return float(sum(value * values[self._index_block_value_dict[index]] for index, value in linear_function.dict().items())) @@ -2961,8 +2970,9 @@ def add_alpha_beta_constraints(self): """ W = self._bijectionist._W Z = self._bijectionist._Z - AZ_matrix = [[ZZ(0)]*len(W) for _ in range(len(Z))] - B_matrix = [[ZZ(0)]*len(W) for _ in range(len(Z))] + zero = self.milp.linear_functions_parent().zero() + AZ_matrix = [[zero]*len(W) for _ in range(len(Z))] + B_matrix = [[zero]*len(W) for _ in range(len(Z))] W_dict = {w: i for i, w in enumerate(W)} Z_dict = {z: i for i, z in enumerate(Z)} @@ -2979,16 +2989,10 @@ def add_alpha_beta_constraints(self): z_index = Z_dict[self._bijectionist._tau[b]] B_matrix[z_index][w_index] += 1 - # TODO: not sure that this is the best way to filter out - # empty conditions for w in range(len(W)): for z in range(len(Z)): - c = AZ_matrix[z][w] - B_matrix[z][w] - if c.is_zero(): - continue - if c in ZZ: - raise MIPSolverException - self.milp.add_constraint(c == 0, name="statistics") + self.milp.add_constraint(AZ_matrix[z][w] == B_matrix[z][w], + name="statistics") def add_distribution_constraints(self): r""" @@ -3026,9 +3030,10 @@ def add_distribution_constraints(self): """ Z = self._bijectionist._Z Z_dict = {z: i for i, z in enumerate(Z)} + zero = self.milp.linear_functions_parent().zero() for tA, tZ in self._bijectionist._elements_distributions: - tA_sum = [ZZ(0)]*len(Z_dict) - tZ_sum = [ZZ(0)]*len(Z_dict) + tA_sum = [zero]*len(Z_dict) + tZ_sum = [zero]*len(Z_dict) for a in tA: p = self._bijectionist._P.find(a) for z in self._bijectionist._possible_block_values[p]: @@ -3036,15 +3041,8 @@ def add_distribution_constraints(self): for z in tZ: tZ_sum[Z_dict[z]] += 1 - # TODO: not sure that this is the best way to filter out - # empty conditions for a, z in zip(tA_sum, tZ_sum): - c = a - z - if c.is_zero(): - continue - if c in ZZ: - raise MIPSolverException - self.milp.add_constraint(c == 0, name=f"d: {a} == {z}") + self.milp.add_constraint(a == z, name=f"d: {a} == {z}") def add_intertwining_relation_constraints(self, origins): r""" @@ -3180,6 +3178,7 @@ def add_pseudo_inverse_relation_constraints(self): else: self.milp.add_constraint(self._x[p, z] == 0, name=f"i: s({p})!={z}") + def _invert_dict(d): """ Return the dictionary whose keys are the values of the input and @@ -3298,7 +3297,7 @@ def _non_copying_intersection(sets): sage: A = sum(As, []) sage: respects_c1 = lambda s: all(c1(a1, a2) not in A or s[c1(a1, a2)] == c1(s[a1], s[a2]) for a1 in A for a2 in A) sage: respects_c2 = lambda s: all(c2(a1) not in A or s[c2(a1)] == c2(s[a1]) for a1 in A) - sage: l2 = [s for s in it if respects_c1(s) and respects_c2(s)] # long time -- (17 seconds with SCIP on AMD Ryzen 5 PRO 3500U w/ Radeon Vega Mobile Gfx) + sage: l2 = [s for s in it if respects_c1(s) and respects_c2(s)] # long time -- (17 seconds on AMD Ryzen 5 PRO 3500U w/ Radeon Vega Mobile Gfx) sage: sorted(l1, key=lambda s: tuple(s.items())) == l2 # long time True From a3364dec31f6eee2ca1c2f2777a39df5a57a716a Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Thu, 29 Dec 2022 22:47:50 +0100 Subject: [PATCH 29/51] move _show to the _BijectionistMILP class --- src/sage/combinat/bijectionist.py | 131 +++++++++++++++--------------- 1 file changed, 66 insertions(+), 65 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 980668f7c7b..93c4867ed0b 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -2548,7 +2548,7 @@ def solutions_iterator(self): solution_index += 1 if get_verbose() >= 2: print("after vetoing") - self._show_bmilp(bmilp, variables=False) + bmilp.show(variables=False) def _solution(self, bmilp, on_blocks=False): r""" @@ -2585,68 +2585,6 @@ def _solution(self, bmilp, on_blocks=False): break return mapping - def _show_bmilp(self, bmilp, variables=True): - """ - Print the constraints and variables of the current MILP - together with some explanations. - - - EXAMPLES:: - - sage: A = B = ["a", "b", "c"] - sage: bij = Bijectionist(A, B, lambda x: A.index(x) % 2, solver="GLPK") - sage: bij.set_constant_blocks([["a", "b"]]) - sage: next(bij.solutions_iterator()) - {'a': 0, 'b': 0, 'c': 1} - sage: bij._show_bmilp(bij._bmilp) - Constraints are: - block a: 1 <= x_0 + x_1 <= 1 - block c: 1 <= x_2 + x_3 <= 1 - statistics: 2 <= 2 x_0 + x_2 <= 2 - statistics: 1 <= 2 x_1 + x_3 <= 1 - veto: x_0 + x_3 <= 1 - Variables are: - x_0: s(a) = s(b) = 0 - x_1: s(a) = s(b) = 1 - x_2: s(c) = 0 - x_3: s(c) = 1 - """ - print("Constraints are:") - b = bmilp.milp.get_backend() - varid_name = {} - for i in range(b.ncols()): - s = b.col_name(i) - default_name = str(bmilp.milp.linear_functions_parent()({i: 1})) - if s and s != default_name: - varid_name[i] = s - else: - varid_name[i] = default_name - for i, (lb, (indices, values), ub) in enumerate(bmilp.milp.constraints()): - if b.row_name(i): - print(" "+b.row_name(i)+":", end=" ") - if lb is not None: - print(str(ZZ(lb))+" <=", end=" ") - first = True - for j, c in sorted(zip(indices, values)): - c = ZZ(c) - if c == 0: - continue - print((("+ " if (not first and c > 0) else "") + - ("" if c == 1 else - ("- " if c == -1 else - (str(c) + " " if first and c < 0 else - ("- " + str(abs(c)) + " " if c < 0 else str(c) + " ")))) - + varid_name[j]), end=" ") - first = False - # Upper bound - print("<= "+str(ZZ(ub)) if ub is not None else "") - - if variables: - print("Variables are:") - for (p, z), v in bmilp._x.items(): - print(f" {v}: " + "".join([f"s({a}) = " - for a in self._P.root_to_elements_dict()[p]]) + f"{z}") - def _initialize_new_bmilp(self): r""" Initialize a :class:`_BijectionistMILP` and add the current constraints. @@ -2670,7 +2608,7 @@ def _initialize_new_bmilp(self): bmilp.add_pseudo_inverse_relation_constraints() bmilp.add_intertwining_relation_constraints(preimage_blocks) if get_verbose() >= 2: - self._show_bmilp(bmilp) + bmilp.show() return bmilp @@ -2721,6 +2659,69 @@ def __init__(self, bijectionist: Bijectionist): for z in bijectionist._possible_block_values[p]) == 1, name=f"block {p}"[:50]) + def show(self, variables=True): + r""" + Print the constraints and variables of the MILP together + with some explanations. + + EXAMPLES:: + + sage: A = B = ["a", "b", "c"] + sage: bij = Bijectionist(A, B, lambda x: A.index(x) % 2, solver="GLPK") + sage: bij.set_constant_blocks([["a", "b"]]) + sage: next(bij.solutions_iterator()) + {'a': 0, 'b': 0, 'c': 1} + sage: bij._bmilp.show() + Constraints are: + block a: 1 <= x_0 + x_1 <= 1 + block c: 1 <= x_2 + x_3 <= 1 + statistics: 2 <= 2 x_0 + x_2 <= 2 + statistics: 1 <= 2 x_1 + x_3 <= 1 + veto: x_0 + x_3 <= 1 + Variables are: + x_0: s(a) = s(b) = 0 + x_1: s(a) = s(b) = 1 + x_2: s(c) = 0 + x_3: s(c) = 1 + + """ + print("Constraints are:") + b = self.milp.get_backend() + varid_name = {} + for i in range(b.ncols()): + s = b.col_name(i) + default_name = str(self.milp.linear_functions_parent()({i: 1})) + if s and s != default_name: + varid_name[i] = s + else: + varid_name[i] = default_name + for i, (lb, (indices, values), ub) in enumerate(self.milp.constraints()): + if b.row_name(i): + print(" "+b.row_name(i)+":", end=" ") + if lb is not None: + print(str(ZZ(lb))+" <=", end=" ") + first = True + for j, c in sorted(zip(indices, values)): + c = ZZ(c) + if c == 0: + continue + print((("+ " if (not first and c > 0) else "") + + ("" if c == 1 else + ("- " if c == -1 else + (str(c) + " " if first and c < 0 else + ("- " + str(abs(c)) + " " if c < 0 else str(c) + " ")))) + + varid_name[j]), end=" ") + first = False + # Upper bound + print("<= "+str(ZZ(ub)) if ub is not None else "") + + if variables: + print("Variables are:") + P = self._bijectionist._P.root_to_elements_dict() + for (p, z), v in self._x.items(): + print(f" {v}: " + "".join([f"s({a}) = " + for a in P[p]]) + f"{z}") + def solve(self, additional_constraints, solution_index=0): r""" Return a solution satisfying the given additional constraints. @@ -2896,7 +2897,7 @@ def _veto_current_solution(self): sage: iter = bij.solutions_iterator() sage: next(iter) # indirect doctest {'a': 0, 'b': 0, 'c': 1} - sage: bij._show_bmilp(bij._bmilp) + sage: bij._bmilp.show() Constraints are: block a: 1 <= x_0 + x_1 <= 1 block c: 1 <= x_2 + x_3 <= 1 From 9d83a6c50c0da26065e7fda350b7a7980e98f52e Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Fri, 30 Dec 2022 14:43:30 +0100 Subject: [PATCH 30/51] remove unused code for backends that do not support removing constraints --- src/sage/combinat/bijectionist.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 93c4867ed0b..64ec91f1d17 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -2814,23 +2814,15 @@ def solve(self, additional_constraints, solution_index=0): return self.last_solution # otherwise generate a new one - # set copy = True if the solver requires us to copy the program and throw it away again - copy = False - if copy: - tmp_milp = deepcopy(self.milp) - for constraint in additional_constraints: - tmp_milp.add_constraint(constraint, return_indices=True) - tmp_milp.solve() - else: - new_indices = [] - for constraint in additional_constraints: - new_indices.extend(self.milp.add_constraint(constraint, return_indices=True)) - try: - self.milp.solve() - self.last_solution = self.milp.get_values(self._x, - convert=bool, tolerance=0.1) - finally: - self.milp.remove_constraints(new_indices) + new_indices = [] + for constraint in additional_constraints: + new_indices.extend(self.milp.add_constraint(constraint, return_indices=True)) + try: + self.milp.solve() + self.last_solution = self.milp.get_values(self._x, + convert=bool, tolerance=0.1) + finally: + self.milp.remove_constraints(new_indices) self._solution_cache.append(self.last_solution) self._veto_current_solution() From 864029dc1800ac3af007a58f50f9f19c2ccb98ac Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Fri, 30 Dec 2022 14:46:46 +0100 Subject: [PATCH 31/51] make _solution a public method of _BijectionistMILP, simplify solve --- src/sage/combinat/bijectionist.py | 232 +++++++++++------------------- 1 file changed, 86 insertions(+), 146 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 64ec91f1d17..01c57830a16 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -1624,10 +1624,10 @@ def _forced_constant_blocks(self): self._bmilp.solve([]) # generate blockwise preimage to determine which blocks have the same image - solution = self._solution(self._bmilp, True) - multiple_preimages = {(value,): preimages - for value, preimages in _invert_dict(solution).items() - if len(preimages) > 1} + solution = self._bmilp.solution(True) + multiple_preimages = {(z,): tP + for z, tP in _invert_dict(solution).items() + if len(tP) > 1} # check for each pair of blocks if a solution with different # values on these block exists @@ -1636,27 +1636,36 @@ def _forced_constant_blocks(self): # multiple_preimages dictionary, restart the check # if no, the two blocks can be joined - # _P has to be copied to not mess with the solution-process + # _P has to be copied to not mess with the solution process # since we do not want to regenerate the bmilp in each step, # so blocks have to stay consistent during the whole process tmp_P = deepcopy(self._P) updated_preimages = True while updated_preimages: updated_preimages = False - for values in copy(multiple_preimages): + for tZ in copy(multiple_preimages): if updated_preimages: break - for i, j in itertools.combinations(copy(multiple_preimages[values]), r=2): + for i, j in itertools.combinations(copy(multiple_preimages[tZ]), r=2): + # veto two blocks having the same value tmp_constraints = [] + for z in self._possible_block_values[i]: + if z in self._possible_block_values[j]: # intersection + tmp_constraints.append(self._bmilp._x[i, z] + self._bmilp._x[j, z] <= 1) try: - # veto the two blocks having the same value - for z in self._possible_block_values[i]: - if z in self._possible_block_values[j]: # intersection - tmp_constraints.append(self._bmilp._x[i, z] + self._bmilp._x[j, z] <= 1) self._bmilp.solve(tmp_constraints) - + except MIPSolverException: + # no solution exists, join blocks + tmp_P.union(i, j) + if i in multiple_preimages[tZ] and j in multiple_preimages[tZ]: + # only one of the joined blocks should remain in the list + multiple_preimages[tZ].remove(j) + if len(multiple_preimages[tZ]) == 1: + del multiple_preimages[tZ] + break + else: # solution exists, update dictionary - solution = self._solution(self._bmilp, True) + solution = self._bmilp.solution(True) updated_multiple_preimages = {} for values in multiple_preimages: for p in multiple_preimages[values]: @@ -1667,15 +1676,6 @@ def _forced_constant_blocks(self): updated_preimages = True multiple_preimages = updated_multiple_preimages break - except MIPSolverException: - # no solution exists, join blocks - tmp_P.union(i, j) - if i in multiple_preimages[values] and j in multiple_preimages[values]: - # only one of the joined blocks should remain in the list - multiple_preimages[values].remove(j) - if len(multiple_preimages[values]) == 1: - del multiple_preimages[values] - break self.set_constant_blocks(tmp_P) @@ -1794,7 +1794,7 @@ def add_solution(solutions, solution): self._bmilp = self._initialize_new_bmilp() self._bmilp.solve([]) - solution = self._solution(self._bmilp) + solution = self._bmilp.solution(False) solutions = {} add_solution(solutions, solution) @@ -1807,7 +1807,7 @@ def add_solution(solutions, solution): try: # problem has a solution, so new value was found self._bmilp.solve(tmp_constraints) - solution = self._solution(self._bmilp) + solution = self._bmilp.solution(False) add_solution(solutions, solution) # veto new value and try again tmp_constraints.append(self._bmilp._x[p, solution[p]] == 0) @@ -1888,7 +1888,7 @@ def minimal_subdistributions_iterator(self): self._bmilp.solve([]) except MIPSolverException: return - s = self._solution(self._bmilp) + s = self._bmilp.solution(False) while True: for v in self._Z: minimal_subdistribution.add_constraint(sum(D[a] for a in self._A if s[a] == v) == V[v]) @@ -1964,7 +1964,7 @@ def _find_counter_example(self, P, s0, d, on_blocks): tmp_constraints = [z_in_d <= z_in_d_count - 1] try: bmilp.solve(tmp_constraints) - return self._solution(bmilp, on_blocks) + return bmilp.solution(on_blocks) except MIPSolverException: pass return @@ -2110,7 +2110,7 @@ def add_counter_example_constraint(s): self._bmilp.solve([]) except MIPSolverException: return - s = self._solution(self._bmilp, True) + s = self._bmilp.solution(True) add_counter_example_constraint(s) while True: try: @@ -2544,47 +2544,12 @@ def solutions_iterator(self): bmilp.solve([], solution_index) except MIPSolverException: return - yield self._solution(bmilp) + yield bmilp.solution(False) solution_index += 1 if get_verbose() >= 2: print("after vetoing") bmilp.show(variables=False) - def _solution(self, bmilp, on_blocks=False): - r""" - Return the current solution as a dictionary from `A` (or - `P`) to `Z`. - - INPUT: - - - ``bmilp``, a :class:`_BijectionistMILP`. - - - ``on_blocks``, whether to return the solution on blocks or - on all elements - - EXAMPLES:: - - sage: A = B = ["a", "b", "c"] - sage: bij = Bijectionist(A, B, lambda x: 0) - sage: bij.set_constant_blocks([["a", "b"]]) - sage: next(bij.solutions_iterator()) - {'a': 0, 'b': 0, 'c': 0} - sage: bij._solution(bij._bmilp, True) - {'a': 0, 'c': 0} - - """ - mapping = {} # A -> Z or P -> Z, a +-> s(a) - for p, block in self._P.root_to_elements_dict().items(): - for z in self._possible_block_values[p]: - if bmilp.has_value(p, z): - if on_blocks: - mapping[p] = z - else: - for a in block: - mapping[a] = z - break - return mapping - def _initialize_new_bmilp(self): r""" Initialize a :class:`_BijectionistMILP` and add the current constraints. @@ -2652,7 +2617,6 @@ def __init__(self, bijectionist: Bijectionist): for z in tZ] self._x = self.milp.new_variable(binary=True, indices=indices) self._solution_cache = [] - self._last_solution = {} for p in _disjoint_set_roots(bijectionist._P): self.milp.add_constraint(sum(self._x[p, z] @@ -2803,12 +2767,7 @@ def solve(self, additional_constraints, solution_index=0): # check whether there is a solution in the cache satisfying # the additional constraints for solution in self._solution_cache[solution_index:]: - if all(all(self._evaluate_linear_function(linear_function, - solution) == value.dict()[-1] - for linear_function, value in constraint.equations()) - and all(self._evaluate_linear_function(linear_function, - solution) <= value.dict()[-1] - for linear_function, value in constraint.inequalities()) + if all(self._is_solution(constraint, solution) for constraint in additional_constraints): self.last_solution = solution return self.last_solution @@ -2824,18 +2783,26 @@ def solve(self, additional_constraints, solution_index=0): finally: self.milp.remove_constraints(new_indices) + # veto the solution, by requiring that not all variables with + # value 1 have value 1 in the new MILP + active_vars = [self._x[p, z] + for p in _disjoint_set_roots(self._bijectionist._P) + for z in self._bijectionist._possible_block_values[p] + if self.last_solution[(p, z)]] + self.milp.add_constraint(sum(active_vars) <= len(active_vars) - 1, + name="veto") + self._solution_cache.append(self.last_solution) - self._veto_current_solution() return self.last_solution - def _evaluate_linear_function(self, linear_function, values): + def _is_solution(self, constraint, values): r""" Evaluate the given function at the given values. INPUT: - - ``linear_function``, a - :class:`sage.numerical.linear_functions.LinearFunction`. + - ``constraint``, a + :class:`sage.numerical.linear_functions.LinearConstraint`. - ``values``, a candidate for a solution of the MILP as a dictionary from pairs `(a, z)\in A\times Z` to `0` or `1`, @@ -2848,92 +2815,65 @@ def _evaluate_linear_function(self, linear_function, values): sage: from sage.combinat.bijectionist import _BijectionistMILP sage: bmilp = _BijectionistMILP(bij) sage: _ = bmilp.solve([]) - sage: f = bmilp._x["a", "a"] + bmilp._x["b", "a"] - sage: v = {('a', 'a'): 1.0, ('a', 'b'): 0.0, ('b', 'a'): 1.0, ('b', 'b'): 0.0} - sage: bmilp._evaluate_linear_function(f, v) - 2.0 + sage: f = bmilp._x["a", "a"] + bmilp._x["b", "a"] >= bmilp._x["b", "b"] + 1 + sage: v = {('a', 'a'): 1, ('a', 'b'): 0, ('b', 'a'): 1, ('b', 'b'): 1} + sage: bmilp._is_solution(f, v) + True + sage: v = {('a', 'a'): 0, ('a', 'b'): 0, ('b', 'a'): 1, ('b', 'b'): 1} + sage: bmilp._is_solution(f, v) + False """ - self._index_block_value_dict = {} + index_block_value_dict = {} for (p, z), v in self._x.items(): variable_index = next(iter(v.dict().keys())) - self._index_block_value_dict[variable_index] = (p, z) - - return float(sum(value * values[self._index_block_value_dict[index]] - for index, value in linear_function.dict().items())) + index_block_value_dict[variable_index] = (p, z) - def _veto_current_solution(self): - r""" - Add a constraint vetoing the current solution. - - This adds a constraint such that the next call to - :meth:`solve` must return a solution different from the - current one. + evaluate = lambda f: sum(coeff if index == -1 else + coeff * values[index_block_value_dict[index]] + for index, coeff in f.dict().items()) - We require that the MILP currently has a solution. + for lhs, rhs in constraint.equations(): + if evaluate(lhs - rhs): + return False + for lhs, rhs in constraint.inequalities(): + if evaluate(lhs - rhs) > 0: + return False + return True - .. WARNING:: + def solution(self, on_blocks): + r""" + Return the current solution as a dictionary from `A` (or + `P`) to `Z`. - The underlying MILP will be modified! + INPUT: - ALGORITHM: + - ``bmilp``, a :class:`_BijectionistMILP`. - We add the constraint `\sum_{x\in V} x < |V|`` where `V` is - the set of variables `x_{p, z}` with value 1, that is, the - set of variables indicating the current solution. + - ``on_blocks``, whether to return the solution on blocks or + on all elements EXAMPLES:: sage: A = B = ["a", "b", "c"] - sage: bij = Bijectionist(A, B, lambda x: A.index(x) % 2) + sage: bij = Bijectionist(A, B, lambda x: 0) sage: bij.set_constant_blocks([["a", "b"]]) - sage: iter = bij.solutions_iterator() - sage: next(iter) # indirect doctest - {'a': 0, 'b': 0, 'c': 1} - sage: bij._bmilp.show() - Constraints are: - block a: 1 <= x_0 + x_1 <= 1 - block c: 1 <= x_2 + x_3 <= 1 - statistics: 2 <= 2 x_0 + x_2 <= 2 - statistics: 1 <= 2 x_1 + x_3 <= 1 - veto: x_0 + x_3 <= 1 - Variables are: - x_0: s(a) = s(b) = 0 - x_1: s(a) = s(b) = 1 - x_2: s(c) = 0 - x_3: s(c) = 1 - """ - # get all variables with value 1 - active_vars = [self._x[p, z] - for p in _disjoint_set_roots(self._bijectionist._P) - for z in self._bijectionist._possible_block_values[p] - if self.last_solution[(p, z)]] - - # add constraint that not all of these can be 1, thus vetoing - # the current solution - self.milp.add_constraint(sum(active_vars) <= len(active_vars) - 1, - name="veto") - - def has_value(self, p, v): - r""" - Return whether a block is mapped to a value in the last solution - computed. - - INPUT: - - - ``p``, the representative of a block - - ``v``, a value in `Z` - - EXAMPLES:: + sage: next(bij.solutions_iterator()) + {'a': 0, 'b': 0, 'c': 0} + sage: bij._bmilp.solution(True) + {'a': 0, 'c': 0} - sage: A = B = ["a", "b"] - sage: bij = Bijectionist(A, B) - sage: from sage.combinat.bijectionist import _BijectionistMILP - sage: bmilp = _BijectionistMILP(bij) - sage: _ = bmilp.solve([bmilp._x["a", "b"] == 1]) - sage: bmilp.has_value("a", "b") - True """ - return self.last_solution[p, v] == 1 + mapping = {} # A -> Z or P -> Z, a +-> s(a) + for p, block in self._bijectionist._P.root_to_elements_dict().items(): + for z in self._bijectionist._possible_block_values[p]: + if self.last_solution[p, z] == 1: + if on_blocks: + mapping[p] = z + else: + for a in block: + mapping[a] = z + break + return mapping def add_alpha_beta_constraints(self): r""" @@ -3012,7 +2952,7 @@ def add_distribution_constraints(self): sage: bmilp = _BijectionistMILP(bij) sage: bmilp.add_distribution_constraints() sage: _ = bmilp.solve([]) - sage: bij._solution(bmilp) + sage: bmilp.solution(False) {[1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 3, @@ -3092,7 +3032,7 @@ def add_intertwining_relation_constraints(self, origins): sage: bmilp = _BijectionistMILP(bij) sage: bmilp.add_intertwining_relation_constraints(preimage_blocks) sage: _ = bmilp.solve([]) - sage: bij._solution(bmilp) + sage: bmilp.solution(False) {'a': 0, 'b': 1, 'c': 0, 'd': 1} """ for composition_index, image_block, preimage_blocks in origins: From 64101a8ffe9fabffa5a33faca001b4292ba6e28d Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Fri, 30 Dec 2022 15:11:40 +0100 Subject: [PATCH 32/51] eliminate _initialize_new_bmilp --- src/sage/combinat/bijectionist.py | 95 +++++++++---------------------- 1 file changed, 27 insertions(+), 68 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 01c57830a16..4da0be9fd9f 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -1620,7 +1620,7 @@ def _forced_constant_blocks(self): """ if self._bmilp is None: - self._bmilp = self._initialize_new_bmilp() + self._bmilp = _BijectionistMILP(self) self._bmilp.solve([]) # generate blockwise preimage to determine which blocks have the same image @@ -1791,7 +1791,7 @@ def add_solution(solutions, solution): # generate initial solution, solution dict and add solution if self._bmilp is None: - self._bmilp = self._initialize_new_bmilp() + self._bmilp = _BijectionistMILP(self) self._bmilp.solve([]) solution = self._bmilp.solution(False) @@ -1884,7 +1884,7 @@ def minimal_subdistributions_iterator(self): try: if self._bmilp is None: - self._bmilp = self._initialize_new_bmilp() + self._bmilp = _BijectionistMILP(self) self._bmilp.solve([]) except MIPSolverException: return @@ -2106,7 +2106,7 @@ def add_counter_example_constraint(s): if self._bmilp is None: try: - self._bmilp = self._initialize_new_bmilp() + self._bmilp = _BijectionistMILP(self) self._bmilp.solve([]) except MIPSolverException: return @@ -2534,7 +2534,7 @@ def solutions_iterator(self): """ if self._bmilp is None: try: - self._bmilp = self._initialize_new_bmilp() + self._bmilp = _BijectionistMILP(self) except MIPSolverException: return bmilp = self._bmilp @@ -2550,32 +2550,6 @@ def solutions_iterator(self): print("after vetoing") bmilp.show(variables=False) - def _initialize_new_bmilp(self): - r""" - Initialize a :class:`_BijectionistMILP` and add the current constraints. - - EXAMPLES:: - - sage: A = B = list('abcd') - sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2) - sage: pi = lambda p1, p2: 'abcdefgh'[A.index(p1) + A.index(p2)] - sage: rho = lambda s1, s2: (s1 + s2) % 2 - sage: bij.set_intertwining_relations((2, pi, rho)) - sage: bij._initialize_new_bmilp() - - """ - preimage_blocks = self._preprocess_intertwining_relations() - self._compute_possible_block_values() - - bmilp = _BijectionistMILP(self) - bmilp.add_alpha_beta_constraints() - bmilp.add_distribution_constraints() - bmilp.add_pseudo_inverse_relation_constraints() - bmilp.add_intertwining_relation_constraints(preimage_blocks) - if get_verbose() >= 2: - bmilp.show() - return bmilp - class _BijectionistMILP(): r""" @@ -2610,18 +2584,26 @@ def __init__(self, bijectionist: Bijectionist): # _elements_distributions # _W, _Z, _A, _B, _P, _alpha, _beta, _tau, _pi_rho, _phi_psi self._bijectionist = bijectionist + preimage_blocks = bijectionist._preprocess_intertwining_relations() + bijectionist._compute_possible_block_values() self.milp = MixedIntegerLinearProgram(solver=bijectionist._solver) self.milp.set_objective(None) + self._solution_cache = [] indices = [(p, z) for p, tZ in bijectionist._possible_block_values.items() for z in tZ] self._x = self.milp.new_variable(binary=True, indices=indices) - self._solution_cache = [] for p in _disjoint_set_roots(bijectionist._P): self.milp.add_constraint(sum(self._x[p, z] for z in bijectionist._possible_block_values[p]) == 1, name=f"block {p}"[:50]) + self.add_alpha_beta_constraints() + self.add_distribution_constraints() + self.add_pseudo_inverse_relation_constraints() + self.add_intertwining_relation_constraints(preimage_blocks) + if get_verbose() >= 2: + self.show() def show(self, variables=True): r""" @@ -2701,31 +2683,12 @@ def solve(self, additional_constraints, solution_index=0): TESTS:: - sage: A = B = ["a", "b"] - sage: bij = Bijectionist(A, B) sage: from sage.combinat.bijectionist import _BijectionistMILP - sage: bmilp = _BijectionistMILP(bij) - sage: len(bmilp._solution_cache) - 0 - - Without any constraints, we do not require that the solution is a bijection:: - - sage: bmilp.solve([bmilp._x["a", "a"] == 1, bmilp._x["b", "a"] == 1]) - {('a', 'a'): True, ('a', 'b'): False, ('b', 'a'): True, ('b', 'b'): False} - sage: len(bmilp._solution_cache) - 1 - sage: bmilp.solve([bmilp._x["a", "b"] == 1, bmilp._x["b", "b"] == 1]) - {('a', 'a'): False, ('a', 'b'): True, ('b', 'a'): False, ('b', 'b'): True} - sage: len(bmilp._solution_cache) - 2 - - A more elaborate test:: - sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)] sage: tau = lambda D: D.number_of_touch_points() sage: bij = Bijectionist(A, B, tau) sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength())) - sage: bmilp = bij._initialize_new_bmilp() + sage: bmilp = _BijectionistMILP(bij) Generate a solution:: @@ -2739,14 +2702,14 @@ def solve(self, additional_constraints, solution_index=0): Generating a new solution that also maps `1010` to `2` fails: - sage: bmilp.solve([bmilp._x[DyckWord([1,0,1,0]), 1] <= 0.5], solution_index=1) + sage: bmilp.solve([bmilp._x[DyckWord([1,0,1,0]), 2] == 1], solution_index=1) Traceback (most recent call last): ... MIPSolverException: ... no feasible solution However, searching for a cached solution succeeds, for inequalities and equalities:: - sage: bmilp.solve([bmilp._x[DyckWord([1,0,1,0]), 1] <= 0.5]) + sage: bmilp.solve([bmilp._x[DyckWord([1,0,1,0]), 2] >= 1]) {([], 0): True, ([1, 0], 1): True, ([1, 0, 1, 0], 1): False, @@ -2762,6 +2725,9 @@ def solve(self, additional_constraints, solution_index=0): ([1, 1, 0, 0], 1): True, ([1, 1, 0, 0], 2): False} + sage: len(bmilp._solution_cache) + 1 + """ assert 0 <= solution_index <= len(self._solution_cache), "the index of the desired solution must not be larger than the number of known solutions" # check whether there is a solution in the cache satisfying @@ -2894,10 +2860,8 @@ def add_alpha_beta_constraints(self): sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)] sage: bij = Bijectionist(A, B, len) sage: bij.set_statistics((len, len)) - sage: bij._compute_possible_block_values() sage: from sage.combinat.bijectionist import _BijectionistMILP - sage: bmilp = _BijectionistMILP(bij) - sage: bmilp.add_alpha_beta_constraints() + sage: bmilp = _BijectionistMILP(bij) # indirect doctest sage: bmilp.solve([]) {([], 0): True, ([1], 1): True, ([1, 2], 2): True, ([2, 1], 2): True} """ @@ -2947,18 +2911,16 @@ def add_distribution_constraints(self): sage: tau = Permutation.longest_increasing_subsequence_length sage: bij = Bijectionist(A, B, tau) sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [1, 3])) - sage: bij._compute_possible_block_values() sage: from sage.combinat.bijectionist import _BijectionistMILP - sage: bmilp = _BijectionistMILP(bij) - sage: bmilp.add_distribution_constraints() + sage: bmilp = _BijectionistMILP(bij) # indirect doctest sage: _ = bmilp.solve([]) sage: bmilp.solution(False) {[1, 2, 3]: 3, [1, 3, 2]: 1, - [2, 1, 3]: 3, - [2, 3, 1]: 3, - [3, 1, 2]: 3, - [3, 2, 1]: 3} + [2, 1, 3]: 2, + [2, 3, 1]: 2, + [3, 1, 2]: 2, + [3, 2, 1]: 2} """ Z = self._bijectionist._Z @@ -3026,11 +2988,8 @@ def add_intertwining_relation_constraints(self, origins): sage: pi = lambda p1, p2: 'abcdefgh'[A.index(p1) + A.index(p2)] sage: rho = lambda s1, s2: (s1 + s2) % 2 sage: bij.set_intertwining_relations((2, pi, rho)) - sage: preimage_blocks = bij._preprocess_intertwining_relations() - sage: bij._compute_possible_block_values() sage: from sage.combinat.bijectionist import _BijectionistMILP - sage: bmilp = _BijectionistMILP(bij) - sage: bmilp.add_intertwining_relation_constraints(preimage_blocks) + sage: bmilp = _BijectionistMILP(bij) # indirect doctest sage: _ = bmilp.solve([]) sage: bmilp.solution(False) {'a': 0, 'b': 1, 'c': 0, 'd': 1} From 2d349114a367d5c7196ad50f1e847dfe466857c7 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Sat, 31 Dec 2022 00:11:50 +0100 Subject: [PATCH 33/51] slightly simplify logic of _forced_constant_blocks, use defaultdict --- src/sage/combinat/bijectionist.py | 175 ++++++++++++++---------------- 1 file changed, 81 insertions(+), 94 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 4da0be9fd9f..b6a80db5acb 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -367,13 +367,13 @@ # https://www.gnu.org/licenses/ # *************************************************************************** import itertools -from collections import namedtuple +from collections import namedtuple, defaultdict from sage.numerical.mip import MixedIntegerLinearProgram, MIPSolverException from sage.rings.integer_ring import ZZ from sage.combinat.set_partition import SetPartition from sage.sets.disjoint_set import DisjointSet from sage.structure.sage_object import SageObject -from copy import copy, deepcopy +from copy import deepcopy from sage.misc.verbose import get_verbose @@ -1419,7 +1419,7 @@ def set_pseudo_inverse_relation(self, *phi_psi): sage: A = B = DyckWords(3) sage: bij = Bijectionist(A, B) sage: bij.set_statistics((lambda D: D.number_of_touch_points(), lambda D: D.number_of_initial_rises())) - sage: ascii_art(list(bij.minimal_subdistributions_iterator())) + sage: ascii_art(sorted(bij.minimal_subdistributions_iterator())) [ ( [ /\ ] ) [ ( [ / \ ] ) ( [ /\ /\ ] [ /\ /\/\ ] ) [ ( [ /\/\/\ ], [ / \ ] ), ( [ /\/ \, / \/\ ], [ / \/\, / \ ] ), @@ -1428,7 +1428,7 @@ def set_pseudo_inverse_relation(self, *phi_psi): ( [ /\/\ / \ ] [ /\ ] ) ] ( [ / \, / \ ], [ /\/\/\, /\/ \ ] ) ] sage: bij.set_pseudo_inverse_relation((lambda D: D, lambda D: D)) - sage: ascii_art(list(bij.minimal_subdistributions_iterator())) + sage: ascii_art(sorted(bij.minimal_subdistributions_iterator())) [ ( [ /\ ] ) [ ( [ / \ ] ) ( [ /\ ] [ /\/\ ] ) [ ( [ /\/\/\ ], [ / \ ] ), ( [ /\/ \ ], [ / \ ] ), @@ -1623,61 +1623,59 @@ def _forced_constant_blocks(self): self._bmilp = _BijectionistMILP(self) self._bmilp.solve([]) - # generate blockwise preimage to determine which blocks have the same image solution = self._bmilp.solution(True) + # multiple_preimages[tZ] are the blocks p which have the same + # value tZ[i] in the i-th known solution multiple_preimages = {(z,): tP for z, tP in _invert_dict(solution).items() if len(tP) > 1} - # check for each pair of blocks if a solution with different - # values on these block exists - - # if yes, use the new solution to update the - # multiple_preimages dictionary, restart the check - # if no, the two blocks can be joined - # _P has to be copied to not mess with the solution process # since we do not want to regenerate the bmilp in each step, # so blocks have to stay consistent during the whole process tmp_P = deepcopy(self._P) - updated_preimages = True - while updated_preimages: - updated_preimages = False - for tZ in copy(multiple_preimages): - if updated_preimages: - break - for i, j in itertools.combinations(copy(multiple_preimages[tZ]), r=2): - # veto two blocks having the same value - tmp_constraints = [] - for z in self._possible_block_values[i]: - if z in self._possible_block_values[j]: # intersection - tmp_constraints.append(self._bmilp._x[i, z] + self._bmilp._x[j, z] <= 1) - try: - self._bmilp.solve(tmp_constraints) - except MIPSolverException: - # no solution exists, join blocks - tmp_P.union(i, j) - if i in multiple_preimages[tZ] and j in multiple_preimages[tZ]: - # only one of the joined blocks should remain in the list - multiple_preimages[tZ].remove(j) - if len(multiple_preimages[tZ]) == 1: - del multiple_preimages[tZ] - break - else: - # solution exists, update dictionary - solution = self._bmilp.solution(True) - updated_multiple_preimages = {} - for values in multiple_preimages: - for p in multiple_preimages[values]: - solution_tuple = (*values, solution[p]) - if solution_tuple not in updated_multiple_preimages: - updated_multiple_preimages[solution_tuple] = [] - updated_multiple_preimages[solution_tuple].append(p) - updated_preimages = True - multiple_preimages = updated_multiple_preimages - break - self.set_constant_blocks(tmp_P) + # check whether blocks p1 and p2 can have different values, + # if so return such a solution + def different_values(p1, p2): + tmp_constraints = [self._bmilp._x[p1, z] + self._bmilp._x[p2, z] <= 1 + for z in self._possible_block_values[p1] + if z in self._possible_block_values[p2]] + try: + self._bmilp.solve(tmp_constraints) + return self._bmilp.solution(True) + except MIPSolverException: + pass + + # try to find a pair of blocks having the same value on all + # known solutions, and a solution such that the values are + # different on this solution + def merge_until_split(): + for tZ in list(multiple_preimages): + tP = multiple_preimages[tZ] + for i2 in range(len(tP)-1, -1, -1): + for i1 in range(i2): + solution = different_values(tP[i1], tP[i2]) + if solution is None: + tmp_P.union(tP[i1], tP[i2]) + if len(multiple_preimages[tZ]) == 2: + del multiple_preimages[tZ] + else: + tP.remove(tP[i2]) + break # skip all pairs (i, j) containing i2 + return solution + + while True: + solution = merge_until_split() + if solution is None: + self.set_constant_blocks(tmp_P) + return + + updated_multiple_preimages = defaultdict(list) + for tZ, tP in multiple_preimages.items(): + for p in tP: + updated_multiple_preimages[tZ + (solution[p],)].append(p) + multiple_preimages = updated_multiple_preimages def possible_values(self, p=None, optimal=False): r""" @@ -1782,38 +1780,34 @@ def possible_values(self, p=None, optimal=False): blocks.add(self._P.find(p2)) if optimal: - # function adding a solution to dict of solutions - def add_solution(solutions, solution): - for p, value in solution.items(): - if p not in solutions: - solutions[p] = set() - solutions[p].add(value) - # generate initial solution, solution dict and add solution if self._bmilp is None: self._bmilp = _BijectionistMILP(self) self._bmilp.solve([]) + solutions = defaultdict(set) solution = self._bmilp.solution(False) - solutions = {} - add_solution(solutions, solution) + for p, z in solution.items(): + solutions[p].add(z) # iterate through blocks and generate all values for p in blocks: tmp_constraints = [] - for value in solutions[p]: - tmp_constraints.append(self._bmilp._x[p, value] == 0) + for z in solutions[p]: + tmp_constraints.append(self._bmilp._x[p, z] == 0) while True: try: # problem has a solution, so new value was found self._bmilp.solve(tmp_constraints) - solution = self._bmilp.solution(False) - add_solution(solutions, solution) - # veto new value and try again - tmp_constraints.append(self._bmilp._x[p, solution[p]] == 0) except MIPSolverException: # no solution, so all possible values have been found break + solution = self._bmilp.solution(False) + for p, z in solution.items(): + solutions[p].add(z) + # veto new value and try again + tmp_constraints.append(self._bmilp._x[p, solution[p]] == 0) + # create dictionary to return possible_values = {} @@ -2210,13 +2204,10 @@ def _preprocess_intertwining_relations(self): something_changed = False # collect (preimage, image) pairs by (representatives) of # the blocks of the elements of the preimage - updated_images = {} # (p_1,...,p_k) to {a_1,....} + updated_images = defaultdict(set) # (p_1,...,p_k) to {a_1,....} for a_tuple, image_set in images.items(): representatives = tuple(self._P.find(a) for a in a_tuple) - if representatives in updated_images: - updated_images[representatives].update(image_set) - else: - updated_images[representatives] = image_set + updated_images[representatives].update(image_set) # merge blocks for a_tuple, image_set in updated_images.items(): @@ -2670,7 +2661,9 @@ def show(self, variables=True): def solve(self, additional_constraints, solution_index=0): r""" - Return a solution satisfying the given additional constraints. + Find a solution satisfying the given additional constraints. + + The solution can then be retrieved using :meth:`solution`. INPUT: @@ -2693,12 +2686,8 @@ def solve(self, additional_constraints, solution_index=0): Generate a solution:: sage: bmilp.solve([]) - {([], 0): True, - ([1, 0], 1): True, - ([1, 0, 1, 0], 1): False, - ([1, 0, 1, 0], 2): True, - ([1, 1, 0, 0], 1): True, - ([1, 1, 0, 0], 2): False} + sage: bmilp.solution(False) + {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1} Generating a new solution that also maps `1010` to `2` fails: @@ -2710,20 +2699,12 @@ def solve(self, additional_constraints, solution_index=0): However, searching for a cached solution succeeds, for inequalities and equalities:: sage: bmilp.solve([bmilp._x[DyckWord([1,0,1,0]), 2] >= 1]) - {([], 0): True, - ([1, 0], 1): True, - ([1, 0, 1, 0], 1): False, - ([1, 0, 1, 0], 2): True, - ([1, 1, 0, 0], 1): True, - ([1, 1, 0, 0], 2): False} + sage: bmilp.solution(False) + {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1} sage: bmilp.solve([bmilp._x[DyckWord([1,0,1,0]), 1] == 0]) - {([], 0): True, - ([1, 0], 1): True, - ([1, 0, 1, 0], 1): False, - ([1, 0, 1, 0], 2): True, - ([1, 1, 0, 0], 1): True, - ([1, 1, 0, 0], 2): False} + sage: bmilp.solution(False) + {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1} sage: len(bmilp._solution_cache) 1 @@ -2736,17 +2717,23 @@ def solve(self, additional_constraints, solution_index=0): if all(self._is_solution(constraint, solution) for constraint in additional_constraints): self.last_solution = solution - return self.last_solution + return # otherwise generate a new one new_indices = [] for constraint in additional_constraints: - new_indices.extend(self.milp.add_constraint(constraint, return_indices=True)) + new_indices.extend(self.milp.add_constraint(constraint, + return_indices=True)) try: self.milp.solve() self.last_solution = self.milp.get_values(self._x, convert=bool, tolerance=0.1) finally: + b = self.milp.get_backend() + if hasattr(b, "_get_model"): + m = b._get_model() + if m.getStatus() != 'unknown': + m.freeTransform() self.milp.remove_constraints(new_indices) # veto the solution, by requiring that not all variables with @@ -2759,7 +2746,6 @@ def solve(self, additional_constraints, solution_index=0): name="veto") self._solution_cache.append(self.last_solution) - return self.last_solution def _is_solution(self, constraint, values): r""" @@ -2863,7 +2849,8 @@ def add_alpha_beta_constraints(self): sage: from sage.combinat.bijectionist import _BijectionistMILP sage: bmilp = _BijectionistMILP(bij) # indirect doctest sage: bmilp.solve([]) - {([], 0): True, ([1], 1): True, ([1, 2], 2): True, ([2, 1], 2): True} + sage: bmilp.solution(False) + {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 2} """ W = self._bijectionist._W Z = self._bijectionist._Z @@ -3032,7 +3019,7 @@ def add_pseudo_inverse_relation_constraints(self): sage: A = B = DyckWords(3) sage: bij = Bijectionist(A, B) sage: bij.set_statistics((lambda D: D.number_of_touch_points(), lambda D: D.number_of_initial_rises())) - sage: ascii_art(list(bij.minimal_subdistributions_iterator())) + sage: ascii_art(sorted(bij.minimal_subdistributions_iterator())) [ ( [ /\ ] ) [ ( [ / \ ] ) ( [ /\ /\ ] [ /\ /\/\ ] ) [ ( [ /\/\/\ ], [ / \ ] ), ( [ /\/ \, / \/\ ], [ / \/\, / \ ] ), @@ -3041,7 +3028,7 @@ def add_pseudo_inverse_relation_constraints(self): ( [ /\/\ / \ ] [ /\ ] ) ] ( [ / \, / \ ], [ /\/\/\, /\/ \ ] ) ] sage: bij.set_pseudo_inverse_relation((lambda D: D, lambda D: D)) # indirect doctest - sage: ascii_art(list(bij.minimal_subdistributions_iterator())) + sage: ascii_art(sorted(bij.minimal_subdistributions_iterator())) [ ( [ /\ ] ) [ ( [ / \ ] ) ( [ /\ ] [ /\/\ ] ) [ ( [ /\/\/\ ], [ / \ ] ), ( [ /\/ \ ], [ / \ ] ), From c2f0062e977fe6ff595862ce72e061d87aaf2d32 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Sat, 31 Dec 2022 00:23:49 +0100 Subject: [PATCH 34/51] slight simplification --- src/sage/combinat/bijectionist.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index b6a80db5acb..b69c620f669 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -1643,9 +1643,9 @@ def different_values(p1, p2): if z in self._possible_block_values[p2]] try: self._bmilp.solve(tmp_constraints) - return self._bmilp.solution(True) except MIPSolverException: - pass + return + return self._bmilp.solution(True) # try to find a pair of blocks having the same value on all # known solutions, and a solution such that the values are @@ -1792,9 +1792,8 @@ def possible_values(self, p=None, optimal=False): # iterate through blocks and generate all values for p in blocks: - tmp_constraints = [] - for z in solutions[p]: - tmp_constraints.append(self._bmilp._x[p, z] == 0) + tmp_constraints = [self._bmilp._x[p, z] == 0 + for z in solutions[p]] while True: try: # problem has a solution, so new value was found @@ -1803,12 +1802,11 @@ def possible_values(self, p=None, optimal=False): # no solution, so all possible values have been found break solution = self._bmilp.solution(False) - for p, z in solution.items(): - solutions[p].add(z) + for p0, z in solution.items(): + solutions[p0].add(z) # veto new value and try again tmp_constraints.append(self._bmilp._x[p, solution[p]] == 0) - # create dictionary to return possible_values = {} for p in blocks: From 78968aebf38b7ca80295a67c296ef0e8df213b7a Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Sat, 31 Dec 2022 14:13:04 +0100 Subject: [PATCH 35/51] copy (instead of deepcopy) should be correct --- src/sage/combinat/bijectionist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index b69c620f669..0e03f3ab6e3 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -373,7 +373,7 @@ from sage.combinat.set_partition import SetPartition from sage.sets.disjoint_set import DisjointSet from sage.structure.sage_object import SageObject -from copy import deepcopy +from copy import copy from sage.misc.verbose import get_verbose @@ -1633,7 +1633,7 @@ def _forced_constant_blocks(self): # _P has to be copied to not mess with the solution process # since we do not want to regenerate the bmilp in each step, # so blocks have to stay consistent during the whole process - tmp_P = deepcopy(self._P) + tmp_P = copy(self._P) # check whether blocks p1 and p2 can have different values, # if so return such a solution From 6ddaeae19c78bb860744fba4bf2b00dc3ef7cd74 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Sat, 31 Dec 2022 20:44:46 +0100 Subject: [PATCH 36/51] slightly simplify _preprocess_intertwining_relations --- src/sage/combinat/bijectionist.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 0e03f3ab6e3..45c73487f09 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -2181,7 +2181,7 @@ def _preprocess_intertwining_relations(self): (0, 'd', ('c', 'b')), (0, 'd', ('d', 'a'))} """ - images = {} # A^k -> A, a_1,...,a_k to pi(a_1,...,a_k), for all pi + images = defaultdict(set) # A^k -> A, a_1,...,a_k +-> {pi(a_1,...,a_k) for all pi} origins_by_elements = [] # (pi/rho, pi(a_1,...,a_k), a_1,...,a_k) for composition_index, pi_rho in enumerate(self._pi_rho): for a_tuple in itertools.product(*([self._A]*pi_rho.numargs)): @@ -2189,11 +2189,7 @@ def _preprocess_intertwining_relations(self): continue a = pi_rho.pi(*a_tuple) if a in self._A: - if a in images: - # this happens if there are several pi's of the same arity - images[a_tuple].add(a) # TODO: wouldn't self._P.find(a) be more efficient here? - else: - images[a_tuple] = set((a,)) # TODO: wouldn't self._P.find(a) be more efficient here? + images[a_tuple].add(a) origins_by_elements.append((composition_index, a, a_tuple)) # merge blocks From 60286e60eafe0206430176074e0b158a6d6f9d0a Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Sat, 31 Dec 2022 20:46:21 +0100 Subject: [PATCH 37/51] slightly improve non_copying_intersection --- src/sage/combinat/bijectionist.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 45c73487f09..ddfe65e454e 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -3111,20 +3111,21 @@ def _non_copying_intersection(sets): sage: _non_copying_intersection([A, B]) is A True + sage: A = set([1,2]); B = set([2,3]) + sage: _non_copying_intersection([A, B]) + {2} + """ sets = sorted(sets, key=len) result = set.intersection(*sets) n = len(result) - if n < len(sets[0]): - return result for s in sets: N = len(s) - if N > n: + if n < N: return result if s == result: return s - """ TESTS:: From c38f5b94d92171a2d18e884f9bdb095ff34ffc65 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Sat, 31 Dec 2022 21:15:50 +0100 Subject: [PATCH 38/51] add some internal documentation --- src/sage/combinat/bijectionist.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index ddfe65e454e..5fe36548a6f 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -2569,8 +2569,13 @@ def __init__(self, bijectionist: Bijectionist): # _elements_distributions # _W, _Z, _A, _B, _P, _alpha, _beta, _tau, _pi_rho, _phi_psi self._bijectionist = bijectionist + # the variables of the MILP are indexed by pairs (p, z), for + # p in _P and z an element of _posible_block_values[p]. + # Thus, _P and _posible_block_values have to be fixed before + # creating the MILP. preimage_blocks = bijectionist._preprocess_intertwining_relations() bijectionist._compute_possible_block_values() + self.milp = MixedIntegerLinearProgram(solver=bijectionist._solver) self.milp.set_objective(None) self._solution_cache = [] From 6fb06f0b17b08528f53e7299d610b67ecf216b11 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Mon, 2 Jan 2023 12:50:50 +0100 Subject: [PATCH 39/51] untangle _preprocess_intertwining_relations --- src/sage/combinat/bijectionist.py | 121 +++++++++++++++--------------- 1 file changed, 62 insertions(+), 59 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 5fe36548a6f..564b2b3d287 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -479,7 +479,7 @@ class Bijectionist(SageObject): """ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], - pi_rho=tuple(), phi_psi=tuple(), + pi_rho=tuple(), phi_psi=tuple(), Q=None, elements_distributions=tuple(), value_restrictions=tuple(), solver=None, key=None): """ @@ -503,11 +503,11 @@ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], # k arity of pi and rho # pi: A^k -> A, rho: Z^k -> Z # a_tuple in A^k - assert len(A) == len(set(A)), "A must have distinct items" - assert len(B) == len(set(B)), "B must have distinct items" + self._A = list(A) + self._B = list(B) + assert len(self._A) == len(set(self._A)), "A must have distinct items" + assert len(self._B) == len(set(self._B)), "B must have distinct items" self._bmilp = None - self._A = A - self._B = B self._sorter = {} self._sorter["A"] = lambda x: sorted(x, key=self._A.index) self._sorter["B"] = lambda x: sorted(x, key=self._B.index) @@ -2127,15 +2127,9 @@ def add_counter_example_constraint(s): def _preprocess_intertwining_relations(self): r""" - Make `self._P` be the finest set partition coarser than `self._P` - such that composing elements preserves blocks. - - OUTPUT: - - A list of triples `((\pi/\rho, p, (p_1,\dots,p_k))`, where - `p` is the block of `\rho(s(a_1),\dots, s(a_k))`, for any - `a_i\in p_i`, suitable for - :meth:`_BijectionistMILP.add_intertwining_relation_constraints`. + Make ``self._P`` be the finest set partition coarser + than ``self._P`` such that composing elements preserves + blocks. Suppose that `p_1`, `p_2` are blocks of `P`, and `a_1, a'_1 \in p_1` and `a_2, a'_2\in p_2`. Then, @@ -2153,14 +2147,17 @@ def _preprocess_intertwining_relations(self): In other words, `s(\pi(a_1,\dots,a_k))` only depends on the blocks of `a_1,\dots,a_k`. + In particular, if `P` consists only if singletons, this + method has no effect. + .. TODO:: create one test with one and one test with two - intertwining_relations + intertwining relations .. TODO:: - untangle side effect and return value if possible + it is not clear, whether this method makes sense EXAMPLES:: @@ -2170,27 +2167,29 @@ def _preprocess_intertwining_relations(self): sage: rho = lambda s1, s2: (s1 + s2) % 2 sage: bij.set_intertwining_relations((2, pi, rho)) sage: bij._preprocess_intertwining_relations() - {(0, 'a', ('a', 'a')), - (0, 'b', ('a', 'b')), - (0, 'b', ('b', 'a')), - (0, 'c', ('a', 'c')), - (0, 'c', ('b', 'b')), - (0, 'c', ('c', 'a')), - (0, 'd', ('a', 'd')), - (0, 'd', ('b', 'c')), - (0, 'd', ('c', 'b')), - (0, 'd', ('d', 'a'))} + sage: bij._P + {{'a'}, {'b'}, {'c'}, {'d'}} + + Let a group act on permutations:: + + sage: A = B = Permutations(3) + sage: bij = Bijectionist(A, B, lambda x: x[0]) + sage: bij.set_intertwining_relations((1, lambda pi: pi.reverse(), lambda z: z)) + sage: bij._preprocess_intertwining_relations() + sage: bij._P + {{[1, 2, 3]}, {[1, 3, 2]}, {[2, 1, 3]}, {[2, 3, 1]}, {[3, 1, 2]}, {[3, 2, 1]}} + """ + A = self._A + P = self._P images = defaultdict(set) # A^k -> A, a_1,...,a_k +-> {pi(a_1,...,a_k) for all pi} - origins_by_elements = [] # (pi/rho, pi(a_1,...,a_k), a_1,...,a_k) for composition_index, pi_rho in enumerate(self._pi_rho): - for a_tuple in itertools.product(*([self._A]*pi_rho.numargs)): + for a_tuple in itertools.product(*([A]*pi_rho.numargs)): if pi_rho.domain is not None and not pi_rho.domain(*a_tuple): continue a = pi_rho.pi(*a_tuple) - if a in self._A: + if a in A: images[a_tuple].add(a) - origins_by_elements.append((composition_index, a, a_tuple)) # merge blocks something_changed = True @@ -2200,27 +2199,20 @@ def _preprocess_intertwining_relations(self): # the blocks of the elements of the preimage updated_images = defaultdict(set) # (p_1,...,p_k) to {a_1,....} for a_tuple, image_set in images.items(): - representatives = tuple(self._P.find(a) for a in a_tuple) + representatives = tuple(P.find(a) for a in a_tuple) updated_images[representatives].update(image_set) # merge blocks for a_tuple, image_set in updated_images.items(): image = image_set.pop() while image_set: - self._P.union(image, image_set.pop()) + P.union(image, image_set.pop()) something_changed = True # we keep a representative image_set.add(image) images = updated_images - origins = set() - for composition_index, image, preimage in origins_by_elements: - origins.add((composition_index, - self._P.find(image), - tuple(self._P.find(a) for a in preimage))) - return origins - def solutions_iterator(self): r""" An iterator over all solutions of the problem. @@ -2573,7 +2565,7 @@ def __init__(self, bijectionist: Bijectionist): # p in _P and z an element of _posible_block_values[p]. # Thus, _P and _posible_block_values have to be fixed before # creating the MILP. - preimage_blocks = bijectionist._preprocess_intertwining_relations() + bijectionist._preprocess_intertwining_relations() bijectionist._compute_possible_block_values() self.milp = MixedIntegerLinearProgram(solver=bijectionist._solver) @@ -2591,7 +2583,7 @@ def __init__(self, bijectionist: Bijectionist): self.add_alpha_beta_constraints() self.add_distribution_constraints() self.add_pseudo_inverse_relation_constraints() - self.add_intertwining_relation_constraints(preimage_blocks) + self.add_intertwining_relation_constraints() if get_verbose() >= 2: self.show() @@ -2925,7 +2917,7 @@ def add_distribution_constraints(self): for a, z in zip(tA_sum, tZ_sum): self.milp.add_constraint(a == z, name=f"d: {a} == {z}") - def add_intertwining_relation_constraints(self, origins): + def add_intertwining_relation_constraints(self): r""" Add constraints corresponding to the given intertwining relations. @@ -2980,23 +2972,34 @@ def add_intertwining_relation_constraints(self, origins): sage: bmilp.solution(False) {'a': 0, 'b': 1, 'c': 0, 'd': 1} """ - for composition_index, image_block, preimage_blocks in origins: - pi_rho = self._bijectionist._pi_rho[composition_index] - # iterate over all possible value combinations of the origin blocks - for z_tuple in itertools.product(*[self._bijectionist._possible_block_values[p] - for p in preimage_blocks]): - rhs = 1 - pi_rho.numargs + sum(self._x[p_i, z_i] - for p_i, z_i in zip(preimage_blocks, z_tuple)) - z = pi_rho.rho(*z_tuple) - if z in self._bijectionist._possible_block_values[image_block]: - c = self._x[image_block, z] - rhs - if c.is_zero(): - continue - self.milp.add_constraint(c >= 0, - name=f"pi/rho({composition_index})") - else: - self.milp.add_constraint(rhs <= 0, - name=f"pi/rho({composition_index})") + A = self._bijectionist._A + tZ = self._bijectionist._possible_block_values + P = self._bijectionist._P + for composition_index, pi_rho in enumerate(self._bijectionist._pi_rho): + pi_blocks = set() + for a_tuple in itertools.product(*([A]*pi_rho.numargs)): + if pi_rho.domain is not None and not pi_rho.domain(*a_tuple): + continue + a = pi_rho.pi(*a_tuple) + if a in A: + p_tuple = tuple(P.find(a) for a in a_tuple) + p = P.find(a) + if (p_tuple, p) not in pi_blocks: + pi_blocks.add((p_tuple, p)) + for z_tuple in itertools.product(*[tZ[p] for p in p_tuple]): + rhs = (1 - pi_rho.numargs + + sum(self._x[p_i, z_i] + for p_i, z_i in zip(p_tuple, z_tuple))) + z = pi_rho.rho(*z_tuple) + if z in tZ[p]: + c = self._x[p, z] - rhs + if c.is_zero(): + continue + self.milp.add_constraint(c >= 0, + name=f"pi/rho({composition_index})") + else: + self.milp.add_constraint(rhs <= 0, + name=f"pi/rho({composition_index})") def add_pseudo_inverse_relation_constraints(self): r""" From 706056e5dbdaf049f56963d2cf67300267899142 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Mon, 2 Jan 2023 13:25:27 +0100 Subject: [PATCH 40/51] add possibility to require a homomesy --- src/sage/combinat/bijectionist.py | 64 ++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 564b2b3d287..106d81c4a49 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -370,7 +370,7 @@ from collections import namedtuple, defaultdict from sage.numerical.mip import MixedIntegerLinearProgram, MIPSolverException from sage.rings.integer_ring import ZZ -from sage.combinat.set_partition import SetPartition +from sage.combinat.set_partition import SetPartition, SetPartitions from sage.sets.disjoint_set import DisjointSet from sage.structure.sage_object import SageObject from copy import copy @@ -534,6 +534,7 @@ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], self.set_value_restrictions(*value_restrictions) self.set_distributions(*elements_distributions) self.set_pseudo_inverse_relation(*phi_psi) + self.set_homomesic(Q) self.set_intertwining_relations(*pi_rho) self.set_constant_blocks(P) @@ -1445,6 +1446,31 @@ def set_pseudo_inverse_relation(self, *phi_psi): self._bmilp = None self._phi_psi = phi_psi + def set_homomesic(self, Q): + """ + Assert that the average of `s` on each block of `Q` is + constant. + + INPUT: + + - ``Q``, a set partition of ``A``. + + EXAMPLES:: + + sage: A = B = [1,2,3] + sage: bij = Bijectionist(A, B, lambda b: b % 3) + sage: bij.set_homomesic([[1,2], [3]]) + sage: list(bij.solutions_iterator()) + [{1: 2, 2: 0, 3: 1}, {1: 0, 2: 2, 3: 1}] + + """ + self._bmilp = None + if Q is None: + self._Q = None + else: + self._Q = SetPartition(Q) + assert self._Q in SetPartitions(self._A), f"{Q} must be a set partition of A" + def _forced_constant_blocks(self): r""" Modify current partition into blocks to the coarsest possible @@ -2583,6 +2609,7 @@ def __init__(self, bijectionist: Bijectionist): self.add_alpha_beta_constraints() self.add_distribution_constraints() self.add_pseudo_inverse_relation_constraints() + self.add_homomesic_constraints() self.add_intertwining_relation_constraints() if get_verbose() >= 2: self.show() @@ -3059,6 +3086,41 @@ def add_pseudo_inverse_relation_constraints(self): else: self.milp.add_constraint(self._x[p, z] == 0, name=f"i: s({p})!={z}") + def add_homomesic_constraints(self): + r""" + Add constraints enforcing that `s` has constant average + on the blocks of `Q`. + + We do this by adding + + .. MATH:: + + \frac{1}{|q|}\sum_{a\in q} \sum_z z x_{p(a), z} = + \frac{1}{|q_0|}\sum_{a\in q_0} \sum_z z x_{p(a), z}, + + for `q\in Q`, where `q_0` is some fixed block of `Q`. + + EXAMPLES:: + + sage: A = B = [1,2,3] + sage: bij = Bijectionist(A, B, lambda b: b % 3) + sage: bij.set_homomesic([[1,2], [3]]) # indirect doctest + sage: list(bij.solutions_iterator()) + [{1: 2, 2: 0, 3: 1}, {1: 0, 2: 2, 3: 1}] + """ + Q = self._bijectionist._Q + if Q is None: + return + P = self._bijectionist._P + tZ = self._bijectionist._possible_block_values + + def sum_q(q): + return sum(sum(z*self._x[P.find(a), z] for z in tZ[P.find(a)]) + for a in q) + q0 = Q[0] + v0 = sum_q(q0) + for q in Q[1:]: + self.milp.add_constraint(len(q0)*sum_q(q) == len(q)*v0, name=f"h: ({q})~({q0})") def _invert_dict(d): """ From 65b0c4adfff850b54c98695d8c027dc20a510ff6 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Tue, 3 Jan 2023 00:58:42 +0100 Subject: [PATCH 41/51] preserve the cache of solutions after computing the optimal constant blocks --- src/sage/combinat/bijectionist.py | 70 +++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 106d81c4a49..1b5adffbf06 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -15,11 +15,14 @@ :widths: 30, 70 :delim: | - :meth:`~Bijectionist.set_intertwining_relations` | Declare that the statistic intertwines with other maps. - :meth:`~Bijectionist.set_constant_blocks` | Declare that the statistic is constant on some sets. :meth:`~Bijectionist.set_statistics` | Declare statistics that are preserved by the bijection. :meth:`~Bijectionist.set_value_restrictions` | Restrict the values of the statistic on an element. + :meth:`~Bijectionist.set_constant_blocks` | Declare that the statistic is constant on some sets. :meth:`~Bijectionist.set_distributions` | Restrict the distribution of values of the statistic on some elements. + :meth:`~Bijectionist.set_intertwining_relations` | Declare that the statistic intertwines with other maps. + :meth:`~Bijectionist.set_pseudo_inverse_relation` | Declare that the statistic satisfies a certain relation. + :meth:`~Bijectionist.set_homomesic` | Declare that the statistic is homomesic with respect to a given set partition. + :meth:`~Bijectionist.statistics_table` | Print a table collecting information on the given statistics. :meth:`~Bijectionist.statistics_fibers` | Collect elements with the same statistics. @@ -1168,7 +1171,7 @@ def set_distributions(self, *elements_distributions): [([[]], [0]), ([[1]], [1]), ([[1, 2, 3]], [3]), - ([[2, 1, 3]], [2]), + ([[2, 3, 1]], [2]), ([[1, 2], [2, 1]], [1, 2])] TESTS: @@ -1694,7 +1697,10 @@ def merge_until_split(): while True: solution = merge_until_split() if solution is None: - self.set_constant_blocks(tmp_P) + self._P = tmp_P + # recreate the MILP + self._bmilp = _BijectionistMILP(self, + self._bmilp._solution_cache) return updated_multiple_preimages = defaultdict(list) @@ -2040,7 +2046,7 @@ def minimal_subdistributions_blocks_iterator(self): sage: bij.constant_blocks(optimal=True) {{'a', 'b'}} sage: list(bij.minimal_subdistributions_blocks_iterator()) - [(['a', 'a', 'c', 'd', 'e'], [1, 1, 2, 2, 3])] + [(['b', 'b', 'c', 'd', 'e'], [1, 1, 2, 2, 3])] An example with overlapping minimal subdistributions:: @@ -2561,7 +2567,7 @@ class is used to manage the MILP, add constraints, solve the problem and check for uniqueness of solution values. """ - def __init__(self, bijectionist: Bijectionist): + def __init__(self, bijectionist: Bijectionist, solutions=None): r""" Initialize the mixed integer linear program. @@ -2569,6 +2575,11 @@ def __init__(self, bijectionist: Bijectionist): - ``bijectionist`` -- an instance of :class:`Bijectionist`. + - ``solutions`` (optional, default: ``None``) -- a list of + solutions of the problem, each provided as a dictionary + mapping `(a, z)` to a Boolean, such that at least one + element from each block of `P` appears as `a`. + .. TODO:: it might be cleaner not to pass the full bijectionist @@ -2581,6 +2592,7 @@ def __init__(self, bijectionist: Bijectionist): sage: from sage.combinat.bijectionist import _BijectionistMILP sage: _BijectionistMILP(bij) + """ # the attributes of the bijectionist class we actually use: # _possible_block_values @@ -2596,15 +2608,15 @@ def __init__(self, bijectionist: Bijectionist): self.milp = MixedIntegerLinearProgram(solver=bijectionist._solver) self.milp.set_objective(None) - self._solution_cache = [] indices = [(p, z) for p, tZ in bijectionist._possible_block_values.items() for z in tZ] self._x = self.milp.new_variable(binary=True, indices=indices) - for p in _disjoint_set_roots(bijectionist._P): - self.milp.add_constraint(sum(self._x[p, z] - for z in bijectionist._possible_block_values[p]) == 1, + tZ = bijectionist._possible_block_values + P = bijectionist._P + for p in _disjoint_set_roots(P): + self.milp.add_constraint(sum(self._x[p, z] for z in tZ[p]) == 1, name=f"block {p}"[:50]) self.add_alpha_beta_constraints() self.add_distribution_constraints() @@ -2614,6 +2626,12 @@ def __init__(self, bijectionist: Bijectionist): if get_verbose() >= 2: self.show() + self._solution_cache = [] + if solutions is not None: + for solution in solutions: + self._add_solution({(P.find(a), z): value + for (a, z), value in solution.items()}) + def show(self, variables=True): r""" Print the constraints and variables of the MILP together @@ -2744,8 +2762,9 @@ def solve(self, additional_constraints, solution_index=0): return_indices=True)) try: self.milp.solve() - self.last_solution = self.milp.get_values(self._x, - convert=bool, tolerance=0.1) + # moving this out of the try...finally block breaks SCIP + solution = self.milp.get_values(self._x, + convert=bool, tolerance=0.1) finally: b = self.milp.get_backend() if hasattr(b, "_get_model"): @@ -2754,16 +2773,23 @@ def solve(self, additional_constraints, solution_index=0): m.freeTransform() self.milp.remove_constraints(new_indices) - # veto the solution, by requiring that not all variables with - # value 1 have value 1 in the new MILP + self._add_solution(solution) + + def _add_solution(self, solution): + r""" + Add the ``last_solution`` to the cache and an + appropriate constraint to the MILP. + + """ active_vars = [self._x[p, z] for p in _disjoint_set_roots(self._bijectionist._P) for z in self._bijectionist._possible_block_values[p] - if self.last_solution[(p, z)]] + if solution[(p, z)]] self.milp.add_constraint(sum(active_vars) <= len(active_vars) - 1, name="veto") + self._solution_cache.append(solution) + self.last_solution = solution - self._solution_cache.append(self.last_solution) def _is_solution(self, constraint, values): r""" @@ -2833,9 +2859,11 @@ def solution(self, on_blocks): {'a': 0, 'c': 0} """ + P = self._bijectionist._P + tZ = self._bijectionist._possible_block_values mapping = {} # A -> Z or P -> Z, a +-> s(a) - for p, block in self._bijectionist._P.root_to_elements_dict().items(): - for z in self._bijectionist._possible_block_values[p]: + for p, block in P.root_to_elements_dict().items(): + for z in tZ[p]: if self.last_solution[p, z] == 1: if on_blocks: mapping[p] = z @@ -3285,9 +3313,9 @@ def _non_copying_intersection(sets): ([[3, 1, 2]], [3]) ([[4, 1, 2, 3]], [4]) ([[5, 1, 2, 3, 4]], [5]) - ([[2, 1, 4, 5, 3], [2, 3, 5, 1, 4], [2, 4, 1, 5, 3], [2, 4, 5, 1, 3]], [2, 3, 3, 3]) - ([[2, 1, 5, 3, 4], [2, 5, 1, 3, 4], [3, 1, 5, 2, 4], [3, 5, 1, 2, 4]], [3, 3, 4, 4]) - ([[1, 3, 2, 5, 4], [1, 3, 5, 2, 4], [1, 4, 2, 5, 3], [1, 4, 5, 2, 3], [1, 4, 5, 3, 2], [1, 5, 4, 2, 3], [1, 5, 4, 3, 2]], [2, 2, 3, 3, 3, 3, 3]) + ([[2, 3, 1, 5, 4], [2, 4, 5, 3, 1], [2, 5, 4, 1, 3], [3, 4, 1, 5, 2]], [2, 3, 3, 3]) + ([[3, 1, 2, 5, 4], [4, 1, 2, 5, 3], [3, 5, 2, 1, 4], [4, 1, 5, 2, 3]], [3, 3, 4, 4]) + ([[2, 1, 3, 5, 4], [2, 4, 1, 3, 5], [2, 5, 3, 1, 4], [3, 4, 1, 2, 5], [3, 1, 5, 4, 2], [2, 5, 1, 4, 3], [2, 1, 5, 4, 3]], [2, 2, 3, 3, 3, 3, 3]) sage: l = list(bij.solutions_iterator()); len(l) # not tested -- (17 seconds with SCIP on AMD Ryzen 5 PRO 3500U w/ Radeon Vega Mobile Gfx) 504 From 21cb54f6e116d3b89db4a4d3789c904cc1ce5177 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Tue, 3 Jan 2023 13:44:06 +0100 Subject: [PATCH 42/51] make _BijectionistMILP.solution the only entrypoint --- src/sage/combinat/bijectionist.py | 236 +++++++++++++----------------- 1 file changed, 103 insertions(+), 133 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 1b5adffbf06..1544adf786a 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -600,7 +600,7 @@ def set_constant_blocks(self, P): sage: bij.constant_blocks(optimal=True) Traceback (most recent call last): ... - MIPSolverException: ... + sage.numerical.mip.MIPSolverException... """ self._bmilp = None @@ -1650,9 +1650,10 @@ def _forced_constant_blocks(self): """ if self._bmilp is None: self._bmilp = _BijectionistMILP(self) - self._bmilp.solve([]) - solution = self._bmilp.solution(True) + solution = self._bmilp.solution(True, []) + if solution is None: + raise MIPSolverException # multiple_preimages[tZ] are the blocks p which have the same # value tZ[i] in the i-th known solution multiple_preimages = {(z,): tP @@ -1670,11 +1671,7 @@ def different_values(p1, p2): tmp_constraints = [self._bmilp._x[p1, z] + self._bmilp._x[p2, z] <= 1 for z in self._possible_block_values[p1] if z in self._possible_block_values[p2]] - try: - self._bmilp.solve(tmp_constraints) - except MIPSolverException: - return - return self._bmilp.solution(True) + return self._bmilp.solution(True, tmp_constraints) # try to find a pair of blocks having the same value on all # known solutions, and a solution such that the values are @@ -1720,14 +1717,35 @@ def possible_values(self, p=None, optimal=False): an element of a block of `P`, or a list of these - ``optimal`` (optional, default: ``False``) -- whether or - not to compute the minimal possible set of statistic values, - throws a MIPSolverException if no solution is found. + not to compute the minimal possible set of statistic values. .. NOTE:: computing the minimal possible set of statistic values may be computationally expensive. + .. TODO:: + + currently, calling this method with ``optimal=True`` does + not update the internal dictionary, because this would + interfere with the variables of the MILP. + + EXAMPLES:: + + sage: A = B = ["a", "b", "c", "d"] + sage: tau = {"a": 1, "b": 1, "c": 1, "d": 2}.get + sage: bij = Bijectionist(A, B, tau) + sage: bij.set_constant_blocks([["a", "b"]]) + sage: bij.possible_values(A) + {'a': {1, 2}, 'b': {1, 2}, 'c': {1, 2}, 'd': {1, 2}} + sage: bij.possible_values(A, optimal=True) + {'a': {1}, 'b': {1}, 'c': {1, 2}, 'd': {1, 2}} + + The internal dictionary is not updated:: + + sage: bij.possible_values(A) + {'a': {1, 2}, 'b': {1, 2}, 'c': {1, 2}, 'd': {1, 2}} + TESTS:: sage: A = B = ["a", "b", "c", "d"] @@ -1746,17 +1764,7 @@ def possible_values(self, p=None, optimal=False): sage: bij.possible_values(p=[["a", "b"], ["c"]]) {'a': {1, 2}, 'b': {1, 2}, 'c': {1, 2}} - Test optimal:: - - sage: bij.possible_values(p=["a", "c"], optimal=True) - {'a': {1, 2}, 'b': {1, 2}, 'c': {1, 2}} - - Verify by listing all solutions:: - - sage: sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))) - [{'a': 1, 'b': 1, 'c': 2, 'd': 2}, {'a': 2, 'b': 2, 'c': 1, 'd': 1}] - - Test if MIPSolverException is thrown:: + Test an unfeasible problem:: sage: A = B = list('ab') sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2) @@ -1764,40 +1772,7 @@ def possible_values(self, p=None, optimal=False): sage: bij.possible_values(p="a") {'a': {0, 1}, 'b': {0, 1}} sage: bij.possible_values(p="a", optimal=True) - Traceback (most recent call last): - ... - sage.numerical.mip.MIPSolverException: ... - - Another example:: - - sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] - sage: tau = Permutation.longest_increasing_subsequence_length - sage: bij = Bijectionist(A, B, tau) - sage: alpha = lambda p: p(1) if len(p) > 0 else 0 - sage: beta = lambda p: p(1) if len(p) > 0 else 0 - sage: bij.set_statistics((alpha, beta), (len, len)) - sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): - ....: print(sol) - {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2} - {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1} - {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2} - {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1} - sage: bij.possible_values(p=[Permutation([1]), Permutation([1, 2, 3]), Permutation([3, 1, 2])], optimal=True) - {[1]: {1}, [1, 2, 3]: {2, 3}, [3, 1, 2]: {1, 2}} - - Another example:: - - sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)] - sage: tau = lambda D: D.number_of_touch_points() - sage: bij = Bijectionist(A, B, tau) - sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength())) - sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): - ....: print(solution) - {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 1, [1, 1, 0, 0]: 2} - {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1} - sage: bij.possible_values(p=[DyckWord([]), DyckWord([1, 0]), DyckWord([1, 0, 1, 0]), DyckWord([1, 1, 0, 0])], optimal=True) - {[]: {0}, [1, 0]: {1}, [1, 0, 1, 0]: {1, 2}, [1, 1, 0, 0]: {1, 2}} - + {'a': set(), 'b': set()} """ # convert input to set of block representatives blocks = set() @@ -1812,32 +1787,25 @@ def possible_values(self, p=None, optimal=False): blocks.add(self._P.find(p2)) if optimal: - # generate initial solution, solution dict and add solution if self._bmilp is None: self._bmilp = _BijectionistMILP(self) - self._bmilp.solve([]) solutions = defaultdict(set) - solution = self._bmilp.solution(False) - for p, z in solution.items(): - solutions[p].add(z) - - # iterate through blocks and generate all values - for p in blocks: - tmp_constraints = [self._bmilp._x[p, z] == 0 - for z in solutions[p]] - while True: - try: - # problem has a solution, so new value was found - self._bmilp.solve(tmp_constraints) - except MIPSolverException: - # no solution, so all possible values have been found - break - solution = self._bmilp.solution(False) - for p0, z in solution.items(): - solutions[p0].add(z) - # veto new value and try again - tmp_constraints.append(self._bmilp._x[p, solution[p]] == 0) + solution = self._bmilp.solution(True, []) + if solution is not None: + for p, z in solution.items(): + solutions[p].add(z) + for p in blocks: + tmp_constraints = [self._bmilp._x[p, z] == 0 + for z in solutions[p]] + while True: + solution = self._bmilp.solution(True, tmp_constraints) + if solution is None: + break + for p0, z in solution.items(): + solutions[p0].add(z) + # veto new value and try again + tmp_constraints.append(self._bmilp._x[p, solution[p]] == 0) # create dictionary to return possible_values = {} @@ -1906,13 +1874,9 @@ def minimal_subdistributions_iterator(self): minimal_subdistribution.set_objective(sum(D[a] for a in self._A)) minimal_subdistribution.add_constraint(sum(D[a] for a in self._A) >= 1) - try: - if self._bmilp is None: - self._bmilp = _BijectionistMILP(self) - self._bmilp.solve([]) - except MIPSolverException: - return - s = self._bmilp.solution(False) + if self._bmilp is None: + self._bmilp = _BijectionistMILP(self) + s = self._bmilp.solution(False, []) while True: for v in self._Z: minimal_subdistribution.add_constraint(sum(D[a] for a in self._A if s[a] == v) == V[v]) @@ -1986,12 +1950,9 @@ def _find_counter_example(self, P, s0, d, on_blocks): # z_in_d_count, because, if the distributions are # different, one such z must exist tmp_constraints = [z_in_d <= z_in_d_count - 1] - try: - bmilp.solve(tmp_constraints) - return bmilp.solution(on_blocks) - except MIPSolverException: - pass - return + solution = bmilp.solution(on_blocks, tmp_constraints) + if solution is not None: + return solution def minimal_subdistributions_blocks_iterator(self): r""" @@ -2129,12 +2090,9 @@ def add_counter_example_constraint(s): if s[p] == v) == V[v]) if self._bmilp is None: - try: - self._bmilp = _BijectionistMILP(self) - self._bmilp.solve([]) - except MIPSolverException: - return - s = self._bmilp.solution(True) + self._bmilp = _BijectionistMILP(self) + + s = self._bmilp.solution(True, []) add_counter_example_constraint(s) while True: try: @@ -2542,19 +2500,15 @@ def solutions_iterator(self): """ if self._bmilp is None: - try: - self._bmilp = _BijectionistMILP(self) - except MIPSolverException: - return + self._bmilp = _BijectionistMILP(self) bmilp = self._bmilp - solution_index = 0 + index = 0 while True: - try: - bmilp.solve([], solution_index) - except MIPSolverException: + solution = bmilp.solution(False, [], index) + if solution is None: return - yield bmilp.solution(False) - solution_index += 1 + index += 1 + yield solution if get_verbose() >= 2: print("after vetoing") bmilp.show(variables=False) @@ -2695,7 +2649,7 @@ def show(self, variables=True): print(f" {v}: " + "".join([f"s({a}) = " for a in P[p]]) + f"{z}") - def solve(self, additional_constraints, solution_index=0): + def _solve(self, additional_constraints, solution_index=0): r""" Find a solution satisfying the given additional constraints. @@ -2721,25 +2675,19 @@ def solve(self, additional_constraints, solution_index=0): Generate a solution:: - sage: bmilp.solve([]) - sage: bmilp.solution(False) + sage: bmilp.solution(False, []) # indirect doctest {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1} Generating a new solution that also maps `1010` to `2` fails: - sage: bmilp.solve([bmilp._x[DyckWord([1,0,1,0]), 2] == 1], solution_index=1) - Traceback (most recent call last): - ... - MIPSolverException: ... no feasible solution + sage: bmilp.solution(False, [bmilp._x[DyckWord([1,0,1,0]), 2] == 1], index=1) However, searching for a cached solution succeeds, for inequalities and equalities:: - sage: bmilp.solve([bmilp._x[DyckWord([1,0,1,0]), 2] >= 1]) - sage: bmilp.solution(False) + sage: bmilp.solution(False, [bmilp._x[DyckWord([1,0,1,0]), 2] >= 1]) {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1} - sage: bmilp.solve([bmilp._x[DyckWord([1,0,1,0]), 1] == 0]) - sage: bmilp.solution(False) + sage: bmilp.solution(False, [bmilp._x[DyckWord([1,0,1,0]), 1] == 0]) {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1} sage: len(bmilp._solution_cache) @@ -2778,8 +2726,32 @@ def solve(self, additional_constraints, solution_index=0): def _add_solution(self, solution): r""" Add the ``last_solution`` to the cache and an - appropriate constraint to the MILP. + appropriate veto constraint to the MILP. + + INPUT: + - ``solution``, a dictionary from the indices of the MILP to + Boolean. + + EXAMPLES:: + + sage: A = B = ["a", "b"] + sage: bij = Bijectionist(A, B) + sage: from sage.combinat.bijectionist import _BijectionistMILP + sage: bmilp = _BijectionistMILP(bij) + sage: bmilp._add_solution({(a, b): a == b for a in A for b in B}) + sage: bmilp.show() # random + Constraints are: + block a: 1 <= x_0 + x_1 <= 1 + block b: 1 <= x_2 + x_3 <= 1 + statistics: 1 <= x_1 + x_3 <= 1 + statistics: 1 <= x_0 + x_2 <= 1 + veto: x_1 + x_2 <= 1 + Variables are: + x_0: s(a) = b + x_1: s(a) = a + x_2: s(b) = b + x_3: s(b) = a """ active_vars = [self._x[p, z] for p in _disjoint_set_roots(self._bijectionist._P) @@ -2790,7 +2762,6 @@ def _add_solution(self, solution): self._solution_cache.append(solution) self.last_solution = solution - def _is_solution(self, constraint, values): r""" Evaluate the given function at the given values. @@ -2810,7 +2781,7 @@ def _is_solution(self, constraint, values): sage: bij = Bijectionist(A, B) sage: from sage.combinat.bijectionist import _BijectionistMILP sage: bmilp = _BijectionistMILP(bij) - sage: _ = bmilp.solve([]) + sage: _ = bmilp._solve([]) sage: f = bmilp._x["a", "a"] + bmilp._x["b", "a"] >= bmilp._x["b", "b"] + 1 sage: v = {('a', 'a'): 1, ('a', 'b'): 0, ('b', 'a'): 1, ('b', 'b'): 1} sage: bmilp._is_solution(f, v) @@ -2836,15 +2807,13 @@ def _is_solution(self, constraint, values): return False return True - def solution(self, on_blocks): + def solution(self, on_blocks, constraints, index=0): r""" - Return the current solution as a dictionary from `A` (or - `P`) to `Z`. + Return a solution as a dictionary from `A` (or `P`) to + `Z`, or ``None`` INPUT: - - ``bmilp``, a :class:`_BijectionistMILP`. - - ``on_blocks``, whether to return the solution on blocks or on all elements @@ -2855,10 +2824,14 @@ def solution(self, on_blocks): sage: bij.set_constant_blocks([["a", "b"]]) sage: next(bij.solutions_iterator()) {'a': 0, 'b': 0, 'c': 0} - sage: bij._bmilp.solution(True) + sage: bij._bmilp.solution(True, []) {'a': 0, 'c': 0} - """ + try: + self._solve(constraints, index) + except MIPSolverException: + return + P = self._bijectionist._P tZ = self._bijectionist._possible_block_values mapping = {} # A -> Z or P -> Z, a +-> s(a) @@ -2894,8 +2867,7 @@ def add_alpha_beta_constraints(self): sage: bij.set_statistics((len, len)) sage: from sage.combinat.bijectionist import _BijectionistMILP sage: bmilp = _BijectionistMILP(bij) # indirect doctest - sage: bmilp.solve([]) - sage: bmilp.solution(False) + sage: bmilp.solution(False, []) {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 2} """ W = self._bijectionist._W @@ -2946,8 +2918,7 @@ def add_distribution_constraints(self): sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [1, 3])) sage: from sage.combinat.bijectionist import _BijectionistMILP sage: bmilp = _BijectionistMILP(bij) # indirect doctest - sage: _ = bmilp.solve([]) - sage: bmilp.solution(False) + sage: bmilp.solution(False, []) {[1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 2, @@ -3023,8 +2994,7 @@ def add_intertwining_relation_constraints(self): sage: bij.set_intertwining_relations((2, pi, rho)) sage: from sage.combinat.bijectionist import _BijectionistMILP sage: bmilp = _BijectionistMILP(bij) # indirect doctest - sage: _ = bmilp.solve([]) - sage: bmilp.solution(False) + sage: bmilp.solution(False, []) {'a': 0, 'b': 1, 'c': 0, 'd': 1} """ A = self._bijectionist._A From a573bb4cdb914a689e20eaaa51c43860f1e48a7d Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Tue, 3 Jan 2023 20:50:18 +0100 Subject: [PATCH 43/51] remove unnecessary calls to list in doctests --- src/sage/combinat/bijectionist.py | 33 ++++++++++++++----------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 1544adf786a..1ebb54ab0dd 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -42,8 +42,7 @@ `(s, wex, fix) \sim (llis, des, adj)`:: sage: N = 3 - sage: As = [list(Permutations(n)) for n in range(N+1)] - sage: A = B = sum(As, []) + sage: A = B = [pi for n in range(N+1) for pi in Permutations(n)] sage: alpha1 = lambda p: len(p.weak_excedences()) sage: alpha2 = lambda p: len(p.fixed_points()) sage: beta1 = lambda p: len(p.descents(final_descent=True)) if p else 0 @@ -106,7 +105,7 @@ +-----------+---+--------+--------+--------+ sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition - sage: bij.set_constant_blocks(sum([orbit_decomposition(A, rotate_permutation) for A in As], [])) + sage: bij.set_constant_blocks(orbit_decomposition(A, rotate_permutation)) sage: bij.constant_blocks() {{[1, 3, 2], [2, 1, 3], [3, 2, 1]}} sage: next(bij.solutions_iterator()) @@ -124,9 +123,9 @@ There is no rotation invariant statistic on non crossing set partitions which is equidistributed with the Strahler number on ordered trees:: - sage: N = 8; As = [[SetPartition(d.to_noncrossing_partition()) for d in DyckWords(n)] for n in range(N)] - sage: A = sum(As, []) - sage: B = sum([list(OrderedTrees(n)) for n in range(1, N+1)], []) + sage: N = 8; + sage: A = [SetPartition(d.to_noncrossing_partition()) for n in range(N) for d in DyckWords(n)] + sage: B = [t for n in range(1, N+1) for t in OrderedTrees(n)] sage: theta = lambda m: SetPartition([[i % m.size() + 1 for i in b] for b in m]) The following code is equivalent to ``tau = findstat(397)``:: @@ -144,7 +143,7 @@ sage: bij = Bijectionist(A, B, tau) sage: bij.set_statistics((lambda a: a.size(), lambda b: b.node_number()-1)) sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition - sage: bij.set_constant_blocks(sum([orbit_decomposition(A_n, theta) for A_n in As], [])) + sage: bij.set_constant_blocks(orbit_decomposition(A, theta)) sage: list(bij.solutions_iterator()) [] @@ -279,7 +278,7 @@ Constant blocks:: - sage: A = B = list('abcd') + sage: A = B = 'abcd' sage: pi = lambda p1, p2: 'abcdefgh'[A.index(p1) + A.index(p2)] sage: rho = lambda s1, s2: (s1 + s2) % 2 sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2, P=[['a', 'c']], pi_rho=((2, pi, rho),)) @@ -492,7 +491,7 @@ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], Check that large input sets are handled well:: - sage: A = B = list(range(20000)) + sage: A = B = range(20000) sage: bij = Bijectionist(A, B) # long time """ # glossary of standard letters: @@ -569,7 +568,7 @@ def set_constant_blocks(self, P): current partition can be reviewed using :meth:`constant_blocks`:: - sage: A = B = list('abcd') + sage: A = B = 'abcd' sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2) sage: bij.constant_blocks() {} @@ -1766,7 +1765,7 @@ def possible_values(self, p=None, optimal=False): Test an unfeasible problem:: - sage: A = B = list('ab') + sage: A = B = 'ab' sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2) sage: bij.set_constant_blocks([['a', 'b']]) sage: bij.possible_values(p="a") @@ -2147,11 +2146,11 @@ def _preprocess_intertwining_relations(self): .. TODO:: - it is not clear, whether this method makes sense + it is not clear whether this method makes sense EXAMPLES:: - sage: A = B = list('abcd') + sage: A = B = 'abcd' sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2) sage: pi = lambda p1, p2: 'abcdefgh'[A.index(p1) + A.index(p2)] sage: rho = lambda s1, s2: (s1 + s2) % 2 @@ -2273,7 +2272,7 @@ def solutions_iterator(self): EXAMPLES:: - sage: A = B = list('abc') + sage: A = B = 'abc' sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2, solver="GLPK") sage: next(bij.solutions_iterator()) {'a': 0, 'b': 1, 'c': 0} @@ -2362,8 +2361,7 @@ def solutions_iterator(self): veto: x_0 + x_1 + x_3 + x_4 + x_6 + x_10 + x_14 <= 6 veto: x_0 + x_1 + x_2 + x_5 + x_6 + x_10 + x_14 <= 6 - Changing or re-setting problem parameters clears the internal cache and - prints even more information:: + Changing or re-setting problem parameters clears the internal cache:: sage: bij.set_constant_blocks(P) sage: _ = list(bij.solutions_iterator()) @@ -2519,7 +2517,6 @@ class _BijectionistMILP(): Wrapper class for the MixedIntegerLinearProgram (MILP). This class is used to manage the MILP, add constraints, solve the problem and check for uniqueness of solution values. - """ def __init__(self, bijectionist: Bijectionist, solutions=None): r""" @@ -2987,7 +2984,7 @@ def add_intertwining_relation_constraints(self): EXAMPLES:: - sage: A = B = list('abcd') + sage: A = B = 'abcd' sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2) sage: pi = lambda p1, p2: 'abcdefgh'[A.index(p1) + A.index(p2)] sage: rho = lambda s1, s2: (s1 + s2) % 2 From 686826127927622f10e97b8f0189ed46d3e55f89 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Tue, 3 Jan 2023 20:50:49 +0100 Subject: [PATCH 44/51] move iterator over all solutions to _BijectionistMILP --- src/sage/combinat/bijectionist.py | 38 ++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 1ebb54ab0dd..6f4833014af 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -2499,17 +2499,7 @@ def solutions_iterator(self): """ if self._bmilp is None: self._bmilp = _BijectionistMILP(self) - bmilp = self._bmilp - index = 0 - while True: - solution = bmilp.solution(False, [], index) - if solution is None: - return - index += 1 - yield solution - if get_verbose() >= 2: - print("after vetoing") - bmilp.show(variables=False) + yield from self._bmilp class _BijectionistMILP(): @@ -2843,6 +2833,32 @@ def solution(self, on_blocks, constraints, index=0): break return mapping + def __iter__(self): + r""" + Iterate over all solutions of the MILP. + + EXAMPLES:: + + sage: A = B = 'abc' + sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2, solver="GLPK") + sage: from sage.combinat.bijectionist import _BijectionistMILP + sage: list(_BijectionistMILP(bij)) + [{'a': 0, 'b': 1, 'c': 0}, + {'a': 1, 'b': 0, 'c': 0}, + {'a': 0, 'b': 0, 'c': 1}] + + """ + index = 0 + while True: + solution = self.solution(False, [], index) + if solution is None: + return + index += 1 + yield solution + if get_verbose() >= 2: + print("after vetoing") + self.show(variables=False) + def add_alpha_beta_constraints(self): r""" Add constraints enforcing that `(alpha, s)` is equidistributed From 61e97bc64d9742026ad8486784b25ea4c730b4f0 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Wed, 4 Jan 2023 01:17:43 +0100 Subject: [PATCH 45/51] merge _solve, solution and __iter__ --- src/sage/combinat/bijectionist.py | 321 +++++++++++------------------- 1 file changed, 120 insertions(+), 201 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 6f4833014af..279ce091c3a 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -599,7 +599,7 @@ def set_constant_blocks(self, P): sage: bij.constant_blocks(optimal=True) Traceback (most recent call last): ... - sage.numerical.mip.MIPSolverException... + StopIteration """ self._bmilp = None @@ -1650,9 +1650,7 @@ def _forced_constant_blocks(self): if self._bmilp is None: self._bmilp = _BijectionistMILP(self) - solution = self._bmilp.solution(True, []) - if solution is None: - raise MIPSolverException + solution = next(self._bmilp.solutions_iterator(True, [])) # multiple_preimages[tZ] are the blocks p which have the same # value tZ[i] in the i-th known solution multiple_preimages = {(z,): tP @@ -1670,7 +1668,7 @@ def different_values(p1, p2): tmp_constraints = [self._bmilp._x[p1, z] + self._bmilp._x[p2, z] <= 1 for z in self._possible_block_values[p1] if z in self._possible_block_values[p2]] - return self._bmilp.solution(True, tmp_constraints) + return next(self._bmilp.solutions_iterator(True, tmp_constraints)) # try to find a pair of blocks having the same value on all # known solutions, and a solution such that the values are @@ -1680,8 +1678,9 @@ def merge_until_split(): tP = multiple_preimages[tZ] for i2 in range(len(tP)-1, -1, -1): for i1 in range(i2): - solution = different_values(tP[i1], tP[i2]) - if solution is None: + try: + solution = different_values(tP[i1], tP[i2]) + except StopIteration: tmp_P.union(tP[i1], tP[i2]) if len(multiple_preimages[tZ]) == 2: del multiple_preimages[tZ] @@ -1788,23 +1787,26 @@ def possible_values(self, p=None, optimal=False): if optimal: if self._bmilp is None: self._bmilp = _BijectionistMILP(self) - + bmilp = self._bmilp solutions = defaultdict(set) - solution = self._bmilp.solution(True, []) - if solution is not None: + try: + solution = next(bmilp.solutions_iterator(True, [])) + except StopIteration: + pass + else: for p, z in solution.items(): solutions[p].add(z) for p in blocks: - tmp_constraints = [self._bmilp._x[p, z] == 0 - for z in solutions[p]] + tmp_constraints = [bmilp._x[p, z] == 0 for z in solutions[p]] while True: - solution = self._bmilp.solution(True, tmp_constraints) - if solution is None: + try: + solution = next(bmilp.solutions_iterator(True, tmp_constraints)) + except StopIteration: break for p0, z in solution.items(): solutions[p0].add(z) # veto new value and try again - tmp_constraints.append(self._bmilp._x[p, solution[p]] == 0) + tmp_constraints.append(bmilp._x[p, solution[p]] == 0) # create dictionary to return possible_values = {} @@ -1875,7 +1877,7 @@ def minimal_subdistributions_iterator(self): if self._bmilp is None: self._bmilp = _BijectionistMILP(self) - s = self._bmilp.solution(False, []) + s = next(self._bmilp.solutions_iterator(False, [])) while True: for v in self._Z: minimal_subdistribution.add_constraint(sum(D[a] for a in self._A if s[a] == v) == V[v]) @@ -1949,9 +1951,11 @@ def _find_counter_example(self, P, s0, d, on_blocks): # z_in_d_count, because, if the distributions are # different, one such z must exist tmp_constraints = [z_in_d <= z_in_d_count - 1] - solution = bmilp.solution(on_blocks, tmp_constraints) - if solution is not None: + try: + solution = next(bmilp.solutions_iterator(on_blocks, tmp_constraints)) return solution + except StopIteration: + pass def minimal_subdistributions_blocks_iterator(self): r""" @@ -2091,7 +2095,7 @@ def add_counter_example_constraint(s): if self._bmilp is None: self._bmilp = _BijectionistMILP(self) - s = self._bmilp.solution(True, []) + s = next(self._bmilp.solutions_iterator(True, [])) add_counter_example_constraint(s) while True: try: @@ -2310,59 +2314,10 @@ def solutions_iterator(self): {[]: 0, [1]: 0, [1, 2]: 1, [2, 1]: 0, [1, 2, 3]: 0, [1, 3, 2]: 1, [2, 1, 3]: 1, [3, 2, 1]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2} {[]: 0, [1]: 0, [1, 2]: 0, [2, 1]: 1, [1, 2, 3]: 0, [1, 3, 2]: 1, [2, 1, 3]: 1, [3, 2, 1]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2} - Setting the verbosity prints the MILP which is solved:: + Changing or re-setting problem parameters clears the internal + cache. Setting the verbosity prints the MILP which is solved.:: sage: set_verbose(2) - sage: _ = list(bij.solutions_iterator()) - after vetoing - Constraints are: - block []: 1 <= x_0 <= 1 - block [1]: 1 <= x_1 <= 1 - block [1, 2]: 1 <= x_2 + x_3 <= 1 - block [2, 1]: 1 <= x_4 + x_5 <= 1 - block [1, 2, 3]: 1 <= x_6 + x_7 + x_8 <= 1 - block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1 - block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1 - statistics: 1 <= x_0 <= 1 - statistics: 0 <= <= 0 - statistics: 0 <= <= 0 - statistics: 1 <= x_1 <= 1 - statistics: 0 <= <= 0 - statistics: 0 <= <= 0 - statistics: 1 <= x_2 + x_4 <= 1 - statistics: 1 <= x_3 + x_5 <= 1 - statistics: 0 <= <= 0 - statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1 - statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3 - statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2 - veto: x_0 + x_1 + x_3 + x_4 + x_6 + x_10 + x_14 <= 6 - veto: x_0 + x_1 + x_2 + x_5 + x_6 + x_10 + x_14 <= 6 - after vetoing - Constraints are: - block []: 1 <= x_0 <= 1 - block [1]: 1 <= x_1 <= 1 - block [1, 2]: 1 <= x_2 + x_3 <= 1 - block [2, 1]: 1 <= x_4 + x_5 <= 1 - block [1, 2, 3]: 1 <= x_6 + x_7 + x_8 <= 1 - block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1 - block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1 - statistics: 1 <= x_0 <= 1 - statistics: 0 <= <= 0 - statistics: 0 <= <= 0 - statistics: 1 <= x_1 <= 1 - statistics: 0 <= <= 0 - statistics: 0 <= <= 0 - statistics: 1 <= x_2 + x_4 <= 1 - statistics: 1 <= x_3 + x_5 <= 1 - statistics: 0 <= <= 0 - statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1 - statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3 - statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2 - veto: x_0 + x_1 + x_3 + x_4 + x_6 + x_10 + x_14 <= 6 - veto: x_0 + x_1 + x_2 + x_5 + x_6 + x_10 + x_14 <= 6 - - Changing or re-setting problem parameters clears the internal cache:: - sage: bij.set_constant_blocks(P) sage: _ = list(bij.solutions_iterator()) Constraints are: @@ -2499,8 +2454,7 @@ def solutions_iterator(self): """ if self._bmilp is None: self._bmilp = _BijectionistMILP(self) - yield from self._bmilp - + yield from self._bmilp.solutions_iterator(False, []) class _BijectionistMILP(): r""" @@ -2636,84 +2590,115 @@ def show(self, variables=True): print(f" {v}: " + "".join([f"s({a}) = " for a in P[p]]) + f"{z}") - def _solve(self, additional_constraints, solution_index=0): - r""" - Find a solution satisfying the given additional constraints. - The solution can then be retrieved using :meth:`solution`. + def _prepare_solution(self, on_blocks, solution): + r""" + Return the solution as a dictionary from `A` (or `P`) to + `Z`. INPUT: - - ``additional_constraints`` -- a list of constraints for the - underlying MILP - - - ``solution_index`` (optional, default: ``0``) -- an index - specifying how many of the solutions in the cache should be - ignored. + - ``on_blocks``, whether to return the solution on blocks or + on all elements TESTS:: - sage: from sage.combinat.bijectionist import _BijectionistMILP - sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)] - sage: tau = lambda D: D.number_of_touch_points() - sage: bij = Bijectionist(A, B, tau) - sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength())) - sage: bmilp = _BijectionistMILP(bij) - - Generate a solution:: + sage: A = B = ["a", "b", "c"] + sage: bij = Bijectionist(A, B, lambda x: 0) + sage: bij.set_constant_blocks([["a", "b"]]) + sage: next(bij.solutions_iterator()) + {'a': 0, 'b': 0, 'c': 0} + sage: bmilp = bij._bmilp + sage: bmilp._prepare_solution(True, bmilp._solution_cache[0]) + {'a': 0, 'c': 0} - sage: bmilp.solution(False, []) # indirect doctest - {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1} + """ + P = self._bijectionist._P + tZ = self._bijectionist._possible_block_values + mapping = {} # A -> Z or P -> Z, a +-> s(a) + for p, block in P.root_to_elements_dict().items(): + for z in tZ[p]: + if solution[p, z] == 1: + if on_blocks: + mapping[p] = z + else: + for a in block: + mapping[a] = z + break + return mapping - Generating a new solution that also maps `1010` to `2` fails: + def solutions_iterator(self, on_blocks, additional_constraints): + r""" + Iterate over all solutions satisfying the additional constraints. - sage: bmilp.solution(False, [bmilp._x[DyckWord([1,0,1,0]), 2] == 1], index=1) + INPUT: - However, searching for a cached solution succeeds, for inequalities and equalities:: + - ``additional_constraints`` -- a list of constraints for the + underlying MILP. - sage: bmilp.solution(False, [bmilp._x[DyckWord([1,0,1,0]), 2] >= 1]) - {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1} + - ``on_blocks``, whether to return the solution on blocks or + on all elements. - sage: bmilp.solution(False, [bmilp._x[DyckWord([1,0,1,0]), 1] == 0]) - {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1} + TESTS:: - sage: len(bmilp._solution_cache) - 1 + sage: A = B = 'abc' + sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2, solver="GLPK") + sage: from sage.combinat.bijectionist import _BijectionistMILP + sage: bmilp = _BijectionistMILP(bij) + sage: it = bmilp.solutions_iterator(False, []) + sage: it2 = bmilp.solutions_iterator(False, [bmilp._x[('c', 1)] == 1]) + sage: next(it) + {'a': 0, 'b': 1, 'c': 0} + sage: next(it2) + {'a': 0, 'b': 0, 'c': 1} + sage: next(it) + {'a': 0, 'b': 0, 'c': 1} + sage: next(it) + {'a': 1, 'b': 0, 'c': 0} """ - assert 0 <= solution_index <= len(self._solution_cache), "the index of the desired solution must not be larger than the number of known solutions" - # check whether there is a solution in the cache satisfying - # the additional constraints - for solution in self._solution_cache[solution_index:]: - if all(self._is_solution(constraint, solution) - for constraint in additional_constraints): - self.last_solution = solution - return - - # otherwise generate a new one - new_indices = [] - for constraint in additional_constraints: - new_indices.extend(self.milp.add_constraint(constraint, - return_indices=True)) - try: - self.milp.solve() - # moving this out of the try...finally block breaks SCIP - solution = self.milp.get_values(self._x, - convert=bool, tolerance=0.1) - finally: - b = self.milp.get_backend() - if hasattr(b, "_get_model"): - m = b._get_model() - if m.getStatus() != 'unknown': - m.freeTransform() - self.milp.remove_constraints(new_indices) - - self._add_solution(solution) + i = 0 # the first unconsidered element of _solution_cache + while True: + # skip solutions which do not satisfy additional_constraints + while i < len(self._solution_cache): + solution = self._solution_cache[i] + i += 1 + if all(self._is_solution(constraint, solution) + for constraint in additional_constraints): + yield self._prepare_solution(on_blocks, solution) + break + else: + new_indices = [] + for constraint in additional_constraints: + new_indices.extend(self.milp.add_constraint(constraint, + return_indices=True)) + try: + self.milp.solve() + # moving this out of the try...finally block breaks SCIP + solution = self.milp.get_values(self._x, + convert=bool, tolerance=0.1) + except MIPSolverException: + return + finally: + b = self.milp.get_backend() + if hasattr(b, "_get_model"): + m = b._get_model() + if m.getStatus() != 'unknown': + m.freeTransform() + self.milp.remove_constraints(new_indices) + + self._add_solution(solution) + i += 1 + assert i == len(self._solution_cache) + yield self._prepare_solution(on_blocks, solution) + if get_verbose() >= 2: + print("after vetoing") + self.show(variables=False) def _add_solution(self, solution): r""" - Add the ``last_solution`` to the cache and an - appropriate veto constraint to the MILP. + Add the ``solution`` to the cache and an appropriate + veto constraint to the MILP. INPUT: @@ -2739,6 +2724,7 @@ def _add_solution(self, solution): x_1: s(a) = a x_2: s(b) = b x_3: s(b) = a + """ active_vars = [self._x[p, z] for p in _disjoint_set_roots(self._bijectionist._P) @@ -2747,7 +2733,6 @@ def _add_solution(self, solution): self.milp.add_constraint(sum(active_vars) <= len(active_vars) - 1, name="veto") self._solution_cache.append(solution) - self.last_solution = solution def _is_solution(self, constraint, values): r""" @@ -2768,7 +2753,6 @@ def _is_solution(self, constraint, values): sage: bij = Bijectionist(A, B) sage: from sage.combinat.bijectionist import _BijectionistMILP sage: bmilp = _BijectionistMILP(bij) - sage: _ = bmilp._solve([]) sage: f = bmilp._x["a", "a"] + bmilp._x["b", "a"] >= bmilp._x["b", "b"] + 1 sage: v = {('a', 'a'): 1, ('a', 'b'): 0, ('b', 'a'): 1, ('b', 'b'): 1} sage: bmilp._is_solution(f, v) @@ -2794,71 +2778,6 @@ def _is_solution(self, constraint, values): return False return True - def solution(self, on_blocks, constraints, index=0): - r""" - Return a solution as a dictionary from `A` (or `P`) to - `Z`, or ``None`` - - INPUT: - - - ``on_blocks``, whether to return the solution on blocks or - on all elements - - EXAMPLES:: - - sage: A = B = ["a", "b", "c"] - sage: bij = Bijectionist(A, B, lambda x: 0) - sage: bij.set_constant_blocks([["a", "b"]]) - sage: next(bij.solutions_iterator()) - {'a': 0, 'b': 0, 'c': 0} - sage: bij._bmilp.solution(True, []) - {'a': 0, 'c': 0} - """ - try: - self._solve(constraints, index) - except MIPSolverException: - return - - P = self._bijectionist._P - tZ = self._bijectionist._possible_block_values - mapping = {} # A -> Z or P -> Z, a +-> s(a) - for p, block in P.root_to_elements_dict().items(): - for z in tZ[p]: - if self.last_solution[p, z] == 1: - if on_blocks: - mapping[p] = z - else: - for a in block: - mapping[a] = z - break - return mapping - - def __iter__(self): - r""" - Iterate over all solutions of the MILP. - - EXAMPLES:: - - sage: A = B = 'abc' - sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2, solver="GLPK") - sage: from sage.combinat.bijectionist import _BijectionistMILP - sage: list(_BijectionistMILP(bij)) - [{'a': 0, 'b': 1, 'c': 0}, - {'a': 1, 'b': 0, 'c': 0}, - {'a': 0, 'b': 0, 'c': 1}] - - """ - index = 0 - while True: - solution = self.solution(False, [], index) - if solution is None: - return - index += 1 - yield solution - if get_verbose() >= 2: - print("after vetoing") - self.show(variables=False) - def add_alpha_beta_constraints(self): r""" Add constraints enforcing that `(alpha, s)` is equidistributed @@ -2880,7 +2799,7 @@ def add_alpha_beta_constraints(self): sage: bij.set_statistics((len, len)) sage: from sage.combinat.bijectionist import _BijectionistMILP sage: bmilp = _BijectionistMILP(bij) # indirect doctest - sage: bmilp.solution(False, []) + sage: next(bmilp.solutions_iterator(False, [])) {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 2} """ W = self._bijectionist._W @@ -2931,7 +2850,7 @@ def add_distribution_constraints(self): sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [1, 3])) sage: from sage.combinat.bijectionist import _BijectionistMILP sage: bmilp = _BijectionistMILP(bij) # indirect doctest - sage: bmilp.solution(False, []) + sage: next(bmilp.solutions_iterator(False, [])) {[1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 2, @@ -3007,7 +2926,7 @@ def add_intertwining_relation_constraints(self): sage: bij.set_intertwining_relations((2, pi, rho)) sage: from sage.combinat.bijectionist import _BijectionistMILP sage: bmilp = _BijectionistMILP(bij) # indirect doctest - sage: bmilp.solution(False, []) + sage: next(bmilp.solutions_iterator(False, [])) {'a': 0, 'b': 1, 'c': 0, 'd': 1} """ A = self._bijectionist._A From db8947bfe5f116492b6fc864b36e6d9054bc34cb Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Wed, 4 Jan 2023 21:17:30 +0100 Subject: [PATCH 46/51] pycodestyle stuff --- src/sage/combinat/bijectionist.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 279ce091c3a..b80deb74e0c 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -2456,6 +2456,7 @@ def solutions_iterator(self): self._bmilp = _BijectionistMILP(self) yield from self._bmilp.solutions_iterator(False, []) + class _BijectionistMILP(): r""" Wrapper class for the MixedIntegerLinearProgram (MILP). This @@ -2590,7 +2591,6 @@ def show(self, variables=True): print(f" {v}: " + "".join([f"s({a}) = " for a in P[p]]) + f"{z}") - def _prepare_solution(self, on_blocks, solution): r""" Return the solution as a dictionary from `A` (or `P`) to @@ -2766,9 +2766,10 @@ def _is_solution(self, constraint, values): variable_index = next(iter(v.dict().keys())) index_block_value_dict[variable_index] = (p, z) - evaluate = lambda f: sum(coeff if index == -1 else - coeff * values[index_block_value_dict[index]] - for index, coeff in f.dict().items()) + def evaluate(f): + return sum(coeff if index == -1 else + coeff * values[index_block_value_dict[index]] + for index, coeff in f.dict().items()) for lhs, rhs in constraint.equations(): if evaluate(lhs - rhs): @@ -3052,6 +3053,7 @@ def sum_q(q): for q in Q[1:]: self.milp.add_constraint(len(q0)*sum_q(q) == len(q)*v0, name=f"h: ({q})~({q0})") + def _invert_dict(d): """ Return the dictionary whose keys are the values of the input and @@ -3126,6 +3128,7 @@ def _non_copying_intersection(sets): if s == result: return s + """ TESTS:: From 53506aac25706957bfc01aa3fdc1b58a3ddcdf06 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Fri, 20 Jan 2023 11:53:48 +0100 Subject: [PATCH 47/51] rename pseudo_inverse to quadratic --- src/sage/combinat/bijectionist.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index b80deb74e0c..8a9989176d6 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -20,7 +20,7 @@ :meth:`~Bijectionist.set_constant_blocks` | Declare that the statistic is constant on some sets. :meth:`~Bijectionist.set_distributions` | Restrict the distribution of values of the statistic on some elements. :meth:`~Bijectionist.set_intertwining_relations` | Declare that the statistic intertwines with other maps. - :meth:`~Bijectionist.set_pseudo_inverse_relation` | Declare that the statistic satisfies a certain relation. + :meth:`~Bijectionist.set_quadratic_relation` | Declare that the statistic satisfies a certain relation. :meth:`~Bijectionist.set_homomesic` | Declare that the statistic is homomesic with respect to a given set partition. @@ -535,7 +535,7 @@ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], self.set_statistics(*alpha_beta) self.set_value_restrictions(*value_restrictions) self.set_distributions(*elements_distributions) - self.set_pseudo_inverse_relation(*phi_psi) + self.set_quadratic_relation(*phi_psi) self.set_homomesic(Q) self.set_intertwining_relations(*pi_rho) self.set_constant_blocks(P) @@ -1395,7 +1395,7 @@ def set_intertwining_relations(self, *pi_rho): set_semi_conjugacy = set_intertwining_relations - def set_pseudo_inverse_relation(self, *phi_psi): + def set_quadratic_relation(self, *phi_psi): r""" Add restrictions of the form `s\circ\psi\circ s = \phi`. @@ -1430,7 +1430,7 @@ def set_pseudo_inverse_relation(self, *phi_psi): ( [ /\ ] ) ] ( [ /\/\ / \ ] [ /\ ] ) ] ( [ / \, / \ ], [ /\/\/\, /\/ \ ] ) ] - sage: bij.set_pseudo_inverse_relation((lambda D: D, lambda D: D)) + sage: bij.set_quadratic_relation((lambda D: D, lambda D: D)) sage: ascii_art(sorted(bij.minimal_subdistributions_iterator())) [ ( [ /\ ] ) [ ( [ / \ ] ) ( [ /\ ] [ /\/\ ] ) @@ -2516,7 +2516,7 @@ def __init__(self, bijectionist: Bijectionist, solutions=None): name=f"block {p}"[:50]) self.add_alpha_beta_constraints() self.add_distribution_constraints() - self.add_pseudo_inverse_relation_constraints() + self.add_quadratic_relation_constraints() self.add_homomesic_constraints() self.add_intertwining_relation_constraints() if get_verbose() >= 2: @@ -2959,7 +2959,7 @@ def add_intertwining_relation_constraints(self): self.milp.add_constraint(rhs <= 0, name=f"pi/rho({composition_index})") - def add_pseudo_inverse_relation_constraints(self): + def add_quadratic_relation_constraints(self): r""" Add constraints enforcing that `s\circ\phi\circ s = \psi`. @@ -2987,7 +2987,7 @@ def add_pseudo_inverse_relation_constraints(self): ( [ /\ ] ) ] ( [ /\/\ / \ ] [ /\ ] ) ] ( [ / \, / \ ], [ /\/\/\, /\/ \ ] ) ] - sage: bij.set_pseudo_inverse_relation((lambda D: D, lambda D: D)) # indirect doctest + sage: bij.set_quadratic_relation((lambda D: D, lambda D: D)) # indirect doctest sage: ascii_art(sorted(bij.minimal_subdistributions_iterator())) [ ( [ /\ ] ) [ ( [ / \ ] ) ( [ /\ ] [ /\/\ ] ) From 31bfb691b9b61091aecefae106acf340fbb85367 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Sat, 11 Feb 2023 16:29:49 +0100 Subject: [PATCH 48/51] fix docstrings, simplify some tests --- src/sage/combinat/bijectionist.py | 757 +++++++++++++++--------------- 1 file changed, 372 insertions(+), 385 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 8a9989176d6..783a5ff4714 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -36,319 +36,313 @@ A guided tour ============= - EXAMPLES: - - We find a statistic `s` such that - `(s, wex, fix) \sim (llis, des, adj)`:: - - sage: N = 3 - sage: A = B = [pi for n in range(N+1) for pi in Permutations(n)] - sage: alpha1 = lambda p: len(p.weak_excedences()) - sage: alpha2 = lambda p: len(p.fixed_points()) - sage: beta1 = lambda p: len(p.descents(final_descent=True)) if p else 0 - sage: beta2 = lambda p: len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1]) - sage: tau = Permutation.longest_increasing_subsequence_length - sage: def rotate_permutation(p): - ....: cycle = Permutation(tuple(range(1, len(p)+1))) - ....: return Permutation([cycle.inverse()(p(cycle(i))) for i in range(1, len(p)+1)]) - sage: bij = Bijectionist(A, B, tau) - sage: bij.set_statistics((len, len), (alpha1, beta1), (alpha2, beta2)) - sage: a, b = bij.statistics_table() - sage: table(a, header_row=True, frame=True) - +-----------+--------+--------+--------+ - | a | α_1(a) | α_2(a) | α_3(a) | - +===========+========+========+========+ - | [] | 0 | 0 | 0 | - +-----------+--------+--------+--------+ - | [1] | 1 | 1 | 1 | - +-----------+--------+--------+--------+ - | [1, 2] | 2 | 2 | 2 | - +-----------+--------+--------+--------+ - | [2, 1] | 2 | 1 | 0 | - +-----------+--------+--------+--------+ - | [1, 2, 3] | 3 | 3 | 3 | - +-----------+--------+--------+--------+ - | [1, 3, 2] | 3 | 2 | 1 | - +-----------+--------+--------+--------+ - | [2, 1, 3] | 3 | 2 | 1 | - +-----------+--------+--------+--------+ - | [2, 3, 1] | 3 | 2 | 0 | - +-----------+--------+--------+--------+ - | [3, 1, 2] | 3 | 1 | 0 | - +-----------+--------+--------+--------+ - | [3, 2, 1] | 3 | 2 | 1 | - +-----------+--------+--------+--------+ - - sage: table(b, header_row=True, frame=True) - +-----------+---+--------+--------+--------+ - | b | τ | β_1(b) | β_2(b) | β_3(b) | - +===========+===+========+========+========+ - | [] | 0 | 0 | 0 | 0 | - +-----------+---+--------+--------+--------+ - | [1] | 1 | 1 | 1 | 1 | - +-----------+---+--------+--------+--------+ - | [1, 2] | 2 | 2 | 1 | 0 | - +-----------+---+--------+--------+--------+ - | [2, 1] | 1 | 2 | 2 | 2 | - +-----------+---+--------+--------+--------+ - | [1, 2, 3] | 3 | 3 | 1 | 0 | - +-----------+---+--------+--------+--------+ - | [1, 3, 2] | 2 | 3 | 2 | 1 | - +-----------+---+--------+--------+--------+ - | [2, 1, 3] | 2 | 3 | 2 | 1 | - +-----------+---+--------+--------+--------+ - | [2, 3, 1] | 2 | 3 | 2 | 1 | - +-----------+---+--------+--------+--------+ - | [3, 1, 2] | 2 | 3 | 2 | 0 | - +-----------+---+--------+--------+--------+ - | [3, 2, 1] | 1 | 3 | 3 | 3 | - +-----------+---+--------+--------+--------+ - - sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition - sage: bij.set_constant_blocks(orbit_decomposition(A, rotate_permutation)) - sage: bij.constant_blocks() - {{[1, 3, 2], [2, 1, 3], [3, 2, 1]}} - sage: next(bij.solutions_iterator()) - {[]: 0, - [1]: 1, - [1, 2]: 1, - [1, 2, 3]: 1, - [1, 3, 2]: 2, - [2, 1]: 2, - [2, 1, 3]: 2, - [2, 3, 1]: 2, - [3, 1, 2]: 3, - [3, 2, 1]: 2} - - There is no rotation invariant statistic on non crossing set partitions which is equidistributed - with the Strahler number on ordered trees:: - - sage: N = 8; - sage: A = [SetPartition(d.to_noncrossing_partition()) for n in range(N) for d in DyckWords(n)] - sage: B = [t for n in range(1, N+1) for t in OrderedTrees(n)] - sage: theta = lambda m: SetPartition([[i % m.size() + 1 for i in b] for b in m]) - - The following code is equivalent to ``tau = findstat(397)``:: - - sage: def tau(T): - ....: if len(T) == 0: - ....: return 1 - ....: else: - ....: l = [tau(S) for S in T] - ....: m = max(l) - ....: if l.count(m) == 1: - ....: return m - ....: else: - ....: return m+1 - sage: bij = Bijectionist(A, B, tau) - sage: bij.set_statistics((lambda a: a.size(), lambda b: b.node_number()-1)) - sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition - sage: bij.set_constant_blocks(orbit_decomposition(A, theta)) - sage: list(bij.solutions_iterator()) - [] - - An example identifying `s` and `S`:: - - sage: N = 4 - sage: A = [dyck_word for n in range(1, N) for dyck_word in DyckWords(n)] - sage: B = [binary_tree for n in range(1, N) for binary_tree in BinaryTrees(n)] - sage: concat_path = lambda D1, D2: DyckWord(list(D1) + list(D2)) - sage: concat_tree = lambda B1, B2: concat_path(B1.to_dyck_word(), - ....: B2.to_dyck_word()).to_binary_tree() - sage: bij = Bijectionist(A, B) - sage: bij.set_intertwining_relations((2, concat_path, concat_tree)) - sage: bij.set_statistics((lambda d: d.semilength(), lambda t: t.node_number())) - sage: for D in sorted(bij.minimal_subdistributions_iterator(), key=lambda x: (len(x[0][0]), x)): - ....: ascii_art(D) - ( [ /\ ], [ o ] ) - ( [ o ] ) - ( [ \ ] ) - ( [ /\/\ ], [ o ] ) - ( [ o ] ) - ( [ /\ ] [ / ] ) - ( [ / \ ], [ o ] ) - ( [ o ] ) - ( [ \ ] ) - ( [ o ] ) - ( [ \ ] ) - ( [ /\/\/\ ], [ o ] ) - ( [ o ] ) - ( [ \ ] ) - ( [ o ] ) - ( [ /\ ] [ / ] ) - ( [ /\/ \ ], [ o ] ) - ( [ o ] ) - ( [ /\ ] [ / \ ] ) - ( [ / \/\ ], [ o o ] ) - ( [ o, o ] ) - ( [ / / ] ) - ( [ /\ ] [ o o ] ) - ( [ /\/\ / \ ] [ \ / ] ) - ( [ / \, / \ ], [ o o ] ) - - The output is in a form suitable for FindStat:: - - sage: findmap(list(bij.minimal_subdistributions_iterator())) # optional -- internet - 0: Mp00034 (quality [100]) - 1: Mp00061oMp00023 (quality [100]) - 2: Mp00018oMp00140 (quality [100]) - - TESTS:: - - sage: N = 4; A = B = [permutation for n in range(N) for permutation in Permutations(n)] - sage: theta = lambda pi: Permutation([x+1 if x != len(pi) else 1 for x in pi[-1:]+pi[:-1]]) - sage: def tau(pi): - ....: n = len(pi) - ....: return sum([1 for i in range(1, n+1) for j in range(1, n+1) - ....: if i Date: Sat, 11 Feb 2023 16:35:29 +0100 Subject: [PATCH 49/51] change lambda to def --- src/sage/combinat/bijectionist.py | 114 +++++++++++++++--------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 783a5ff4714..6e756891977 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -42,10 +42,10 @@ sage: N = 3 sage: A = B = [pi for n in range(N+1) for pi in Permutations(n)] - sage: alpha1 = lambda p: len(p.weak_excedences()) - sage: alpha2 = lambda p: len(p.fixed_points()) - sage: beta1 = lambda p: len(p.descents(final_descent=True)) if p else 0 - sage: beta2 = lambda p: len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1]) + sage: def alpha1(p): return len(p.weak_excedences()) + sage: def alpha2(p): return len(p.fixed_points()) + sage: def beta1(p): return len(p.descents(final_descent=True)) if p else 0 + sage: def beta2(p): return len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1]) sage: tau = Permutation.longest_increasing_subsequence_length sage: def rotate_permutation(p): ....: cycle = Permutation(tuple(range(1, len(p)+1))) @@ -125,7 +125,7 @@ sage: N = 8; sage: A = [SetPartition(d.to_noncrossing_partition()) for n in range(N) for d in DyckWords(n)] sage: B = [t for n in range(1, N+1) for t in OrderedTrees(n)] - sage: theta = lambda m: SetPartition([[i % m.size() + 1 for i in b] for b in m]) + sage: def theta(m): return SetPartition([[i % m.size() + 1 for i in b] for b in m]) The following code is equivalent to ``tau = findstat(397)``:: @@ -195,7 +195,7 @@ TESTS:: sage: N = 4; A = B = [permutation for n in range(N) for permutation in Permutations(n)] - sage: theta = lambda pi: Permutation([x+1 if x != len(pi) else 1 for x in pi[-1:]+pi[:-1]]) + sage: def theta(pi): return Permutation([x+1 if x != len(pi) else 1 for x in pi[-1:]+pi[:-1]]) sage: def tau(pi): ....: n = len(pi) ....: return sum([1 for i in range(1, n+1) for j in range(1, n+1) @@ -213,9 +213,9 @@ A test including intertwining relations:: sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)] - sage: alpha = lambda D: (D.area(), D.bounce()) - sage: beta = lambda D: (D.bounce(), D.area()) - sage: tau = lambda D: D.number_of_touch_points() + sage: def alpha(D): return (D.area(), D.bounce()) + sage: def beta(D): return (D.bounce(), D.area()) + sage: def tau(D): return D.number_of_touch_points() The following looks correct:: @@ -263,9 +263,9 @@ Repeating some tests, but using the constructor instead of set_XXX() methods: sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)] - sage: alpha = lambda D: (D.area(), D.bounce()) - sage: beta = lambda D: (D.bounce(), D.area()) - sage: tau = lambda D: D.number_of_touch_points() + sage: def alpha(D): return (D.area(), D.bounce()) + sage: def beta(D): return (D.bounce(), D.area()) + sage: def tau(D): return D.number_of_touch_points() sage: bij = Bijectionist(A, B, tau, alpha_beta=((lambda d: d.semilength(), lambda d: d.semilength()),)) sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): @@ -276,8 +276,8 @@ Constant blocks:: sage: A = B = 'abcd' - sage: pi = lambda p1, p2: 'abcdefgh'[A.index(p1) + A.index(p2)] - sage: rho = lambda s1, s2: (s1 + s2) % 2 + sage: def pi(p1, p2): return 'abcdefgh'[A.index(p1) + A.index(p2)] + sage: def rho(s1, s2): return (s1 + s2) % 2 sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2, P=[['a', 'c']], pi_rho=((2, pi, rho),)) sage: list(bij.solutions_iterator()) [{'a': 0, 'b': 1, 'c': 0, 'd': 1}] @@ -298,7 +298,7 @@ Intertwining relations:: - sage: concat = lambda p1, p2: Permutation(p1 + [i + len(p1) for i in p2]) + sage: def concat(p1, p2): return Permutation(p1 + [i + len(p1) for i in p2]) sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] sage: bij = Bijectionist(A, B, Permutation.number_of_fixed_points, alpha_beta=((len, len),), pi_rho=((2, concat, lambda x, y: x + y),)) @@ -311,9 +311,9 @@ Statistics:: sage: N = 4; A = B = [permutation for n in range(N) for permutation in Permutations(n)] - sage: wex = lambda p: len(p.weak_excedences()) - sage: fix = lambda p: len(p.fixed_points()) - sage: des = lambda p: len(p.descents(final_descent=True)) if p else 0 + sage: def wex(p): return len(p.weak_excedences()) + sage: def fix(p): return len(p.fixed_points()) + sage: def des(p): return len(p.descents(final_descent=True)) if p else 0 sage: bij = Bijectionist(A, B, fix, alpha_beta=((wex, des), (len, len))) sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): ....: print(solution) @@ -567,8 +567,8 @@ def set_constant_blocks(self, P): We now add a map that combines some blocks:: - sage: pi = lambda p1, p2: 'abcdefgh'[A.index(p1) + A.index(p2)] - sage: rho = lambda s1, s2: (s1 + s2) % 2 + sage: def pi(p1, p2): return 'abcdefgh'[A.index(p1) + A.index(p2)] + sage: def rho(s1, s2): return (s1 + s2) % 2 sage: bij.set_intertwining_relations((2, pi, rho)) sage: list(bij.solutions_iterator()) [{'a': 0, 'b': 1, 'c': 0, 'd': 1}] @@ -672,10 +672,10 @@ def set_statistics(self, *alpha_beta): of fixed points of `S(\pi)` equals `s(\pi)`:: sage: N = 4; A = B = [permutation for n in range(N) for permutation in Permutations(n)] - sage: wex = lambda p: len(p.weak_excedences()) - sage: fix = lambda p: len(p.fixed_points()) - sage: des = lambda p: len(p.descents(final_descent=True)) if p else 0 - sage: adj = lambda p: len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1]) + sage: def wex(p): return len(p.weak_excedences()) + sage: def fix(p): return len(p.fixed_points()) + sage: def des(p): return len(p.descents(final_descent=True)) if p else 0 + sage: def adj(p): return len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1]) sage: bij = Bijectionist(A, B, fix) sage: bij.set_statistics((wex, des), (len, len)) sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): @@ -708,9 +708,9 @@ def set_statistics(self, *alpha_beta): Calling ``set_statistics`` without arguments should restore the previous state:: sage: N = 3; A = B = [permutation for n in range(N) for permutation in Permutations(n)] - sage: wex = lambda p: len(p.weak_excedences()) - sage: fix = lambda p: len(p.fixed_points()) - sage: des = lambda p: len(p.descents(final_descent=True)) if p else 0 + sage: def wex(p): return len(p.weak_excedences()) + sage: def fix(p): return len(p.fixed_points()) + sage: def des(p): return len(p.descents(final_descent=True)) if p else 0 sage: bij = Bijectionist(A, B, fix) sage: bij.set_statistics((wex, des), (len, len)) sage: for solution in bij.solutions_iterator(): @@ -783,10 +783,10 @@ def statistics_fibers(self): sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] sage: tau = Permutation.longest_increasing_subsequence_length - sage: wex = lambda p: len(p.weak_excedences()) - sage: fix = lambda p: len(p.fixed_points()) - sage: des = lambda p: len(p.descents(final_descent=True)) if p else 0 - sage: adj = lambda p: len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1]) + sage: def wex(p): return len(p.weak_excedences()) + sage: def fix(p): return len(p.fixed_points()) + sage: def des(p): return len(p.descents(final_descent=True)) if p else 0 + sage: def adj(p): return len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1]) sage: bij = Bijectionist(A, B, tau) sage: bij.set_statistics((len, len), (wex, des), (fix, adj)) sage: table([[key, AB[0], AB[1]] for key, AB in bij.statistics_fibers().items()]) @@ -828,10 +828,10 @@ def statistics_table(self, header=True): sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] sage: tau = Permutation.longest_increasing_subsequence_length - sage: wex = lambda p: len(p.weak_excedences()) - sage: fix = lambda p: len(p.fixed_points()) - sage: des = lambda p: len(p.descents(final_descent=True)) if p else 0 - sage: adj = lambda p: len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1]) + sage: def wex(p): return len(p.weak_excedences()) + sage: def fix(p): return len(p.fixed_points()) + sage: def des(p): return len(p.descents(final_descent=True)) if p else 0 + sage: def adj(p): return len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1]) sage: bij = Bijectionist(A, B, tau) sage: bij.set_statistics((wex, des), (fix, adj)) sage: a, b = bij.statistics_table() @@ -1193,8 +1193,8 @@ def set_distributions(self, *elements_distributions): Another example with statistics:: sage: bij = Bijectionist(A, B, tau) - sage: alpha = lambda p: p(1) if len(p) > 0 else 0 - sage: beta = lambda p: p(1) if len(p) > 0 else 0 + sage: def alpha(p): return p(1) if len(p) > 0 else 0 + sage: def beta(p): return p(1) if len(p) > 0 else 0 sage: bij.set_statistics((alpha, beta), (len, len)) sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): ....: print(sol) @@ -1304,7 +1304,7 @@ def set_intertwining_relations(self, *pi_rho): of the second permutation by the length of the first permutation:: - sage: concat = lambda p1, p2: Permutation(p1 + [i + len(p1) for i in p2]) + sage: def concat(p1, p2): return Permutation(p1 + [i + len(p1) for i in p2]) We may be interested in statistics on permutations which are equidistributed with the number of fixed points, such that @@ -1496,7 +1496,7 @@ def _forced_constant_blocks(self): sage: N = 4 sage: A = B = [permutation for n in range(2, N) for permutation in Permutations(n)] - sage: tau = lambda p: p[0] if len(p) else 0 + sage: def tau(p): return p[0] if len(p) else 0 sage: add_n = lambda p1: Permutation(p1 + [1 + len(p1)]) sage: add_1 = lambda p1: Permutation([1] + [1 + i for i in p1]) sage: bij = Bijectionist(A, B, tau) @@ -1528,8 +1528,8 @@ def _forced_constant_blocks(self): sage: bij.constant_blocks() {{[2, 3, 1], [3, 1, 2]}} - sage: concat = lambda p1, p2: Permutation(p1 + [i + len(p1) for i in p2]) - sage: union = lambda p1, p2: Partition(sorted(list(p1) + list(p2), reverse=True)) + sage: def concat(p1, p2): return Permutation(p1 + [i + len(p1) for i in p2]) + sage: def union(p1, p2): return Partition(sorted(list(p1) + list(p2), reverse=True)) sage: bij.set_intertwining_relations((2, concat, union)) In this case we do not discover constant blocks by looking at the intertwining_relations only:: @@ -1546,10 +1546,10 @@ def _forced_constant_blocks(self): sage: N = 4 sage: A = B = [permutation for n in range(N + 1) for permutation in Permutations(n)] - sage: alpha1 = lambda p: len(p.weak_excedences()) - sage: alpha2 = lambda p: len(p.fixed_points()) - sage: beta1 = lambda p: len(p.descents(final_descent=True)) if p else 0 - sage: beta2 = lambda p: len([e for (e, f) in zip(p, p[1:] + [0]) if e == f + 1]) + sage: def alpha1(p): return len(p.weak_excedences()) + sage: def alpha2(p): return len(p.fixed_points()) + sage: def beta1(p): return len(p.descents(final_descent=True)) if p else 0 + sage: def beta2(p): return len([e for (e, f) in zip(p, p[1:] + [0]) if e == f + 1]) sage: tau = Permutation.longest_increasing_subsequence_length sage: def rotate_permutation(p): ....: cycle = Permutation(tuple(range(1, len(p) + 1))) @@ -1826,7 +1826,7 @@ def minimal_subdistributions_iterator(self): Another example:: sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)] - sage: tau = lambda D: D.number_of_touch_points() + sage: def tau(D): return D.number_of_touch_points() sage: bij = Bijectionist(A, B, tau) sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength())) sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): @@ -1974,7 +1974,7 @@ def minimal_subdistributions_blocks_iterator(self): Another example:: sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)] - sage: tau = lambda D: D.number_of_touch_points() + sage: def tau(D): return D.number_of_touch_points() sage: bij = Bijectionist(A, B, tau) sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength())) sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))): @@ -2143,8 +2143,8 @@ def _preprocess_intertwining_relations(self): sage: A = B = 'abcd' sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2) - sage: pi = lambda p1, p2: 'abcdefgh'[A.index(p1) + A.index(p2)] - sage: rho = lambda s1, s2: (s1 + s2) % 2 + sage: def pi(p1, p2): return 'abcdefgh'[A.index(p1) + A.index(p2)] + sage: def rho(s1, s2): return (s1 + s2) % 2 sage: bij.set_intertwining_relations((2, pi, rho)) sage: bij._preprocess_intertwining_relations() sage: bij._P @@ -2909,8 +2909,8 @@ def add_intertwining_relation_constraints(self): sage: A = B = 'abcd' sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2) - sage: pi = lambda p1, p2: 'abcdefgh'[A.index(p1) + A.index(p2)] - sage: rho = lambda s1, s2: (s1 + s2) % 2 + sage: def pi(p1, p2): return 'abcdefgh'[A.index(p1) + A.index(p2)] + sage: def rho(s1, s2): return (s1 + s2) % 2 sage: bij.set_intertwining_relations((2, pi, rho)) sage: from sage.combinat.bijectionist import _BijectionistMILP sage: bmilp = _BijectionistMILP(bij) # indirect doctest @@ -3127,8 +3127,8 @@ def _non_copying_intersection(sets): Note that adding ``[(2,-2,-1), (2,2,-1), (2,-2,1), (2,2,1)]`` makes it take (seemingly) forever.:: - sage: c1 = lambda a, b: (a[0]+b[0], a[1]*b[1], a[2]*b[2]) - sage: c2 = lambda a: (a[0], -a[1], a[2]) + sage: def c1(a, b): return (a[0]+b[0], a[1]*b[1], a[2]*b[2]) + sage: def c2(a): return (a[0], -a[1], a[2]) sage: bij = Bijectionist(sum(As, []), sum(Bs, [])) sage: bij.set_statistics((lambda x: x[0], lambda x: x[0])) @@ -3168,10 +3168,10 @@ def _non_copying_intersection(sets): Our benchmark example:: sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition - sage: alpha1 = lambda p: len(p.weak_excedences()) - sage: alpha2 = lambda p: len(p.fixed_points()) - sage: beta1 = lambda p: len(p.descents(final_descent=True)) if p else 0 - sage: beta2 = lambda p: len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1]) + sage: def alpha1(p): return len(p.weak_excedences()) + sage: def alpha2(p): return len(p.fixed_points()) + sage: def beta1(p): return len(p.descents(final_descent=True)) if p else 0 + sage: def beta2(p): return len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1]) sage: gamma = Permutation.longest_increasing_subsequence_length sage: def rotate_permutation(p): ....: cycle = Permutation(tuple(range(1, len(p)+1))) From dc3009a0c780e328d4ed18ef36d86a602e1168d1 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Mon, 13 Feb 2023 15:27:54 +0100 Subject: [PATCH 50/51] reviewer's suggestions --- src/sage/combinat/bijectionist.py | 145 +++++++++++++++++------------- 1 file changed, 83 insertions(+), 62 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 6e756891977..8844f4f3602 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- r""" A bijectionist's toolkit @@ -36,9 +35,23 @@ A guided tour ============= -EXAMPLES: +Consider the following combinatorial statistics on a permutation: -We find a statistic `s` such that `(s, wex, fix) \sim (llis, des, adj)`:: + * `wex`, the number of weak excedences, + * `fix`, the number of fixed points, + * `des`, the number of descents (after appending `0`), + * `adj`, the number of adjacencies (after appending `0`), and + * `llis`, the length of a longest increasing subsequence. + +Moreover, let `rot` be action of rotation on a permutation, i.e., the +conjugation with the long cycle. + +It is known that there must exist a statistic `s` on permutations, +which is equidistributed with `llis` but additionally invariant under +`rot`. However, at least very small cases do not contradict the +possibility that one can even find a statistic `s`, invariant under +`rot` and such that `(s, wex, fix) \sim (llis, des, adj)`. Let us +check this for permutations of size at most `3`:: sage: N = 3 sage: A = B = [pi for n in range(N+1) for pi in Permutations(n)] @@ -119,15 +132,17 @@ [3, 1, 2]: 3, [3, 2, 1]: 2} -There is no rotation invariant statistic on non crossing set partitions which -is equidistributed with the Strahler number on ordered trees:: +On the other hand, we can check that there is no rotation invariant +statistic on non-crossing set partitions which is equidistributed +with the Strahler number on ordered trees:: - sage: N = 8; + sage: N = 8 sage: A = [SetPartition(d.to_noncrossing_partition()) for n in range(N) for d in DyckWords(n)] sage: B = [t for n in range(1, N+1) for t in OrderedTrees(n)] sage: def theta(m): return SetPartition([[i % m.size() + 1 for i in b] for b in m]) -The following code is equivalent to ``tau = findstat(397)``:: +Code for the Strahler number can be obtained from FindStat. The +following code is equivalent to ``tau = findstat(397)``:: sage: def tau(T): ....: if len(T) == 0: @@ -146,7 +161,8 @@ sage: list(bij.solutions_iterator()) [] -An example identifying `s` and `S`:: +Next we demonstrate how to search for a bijection, instead An example +identifying `s` and `S`:: sage: N = 4 sage: A = [dyck_word for n in range(1, N) for dyck_word in DyckWords(n)] @@ -342,7 +358,7 @@ sage: bij = Bijectionist(A, B, tau, value_restrictions=((Permutation([1, 2]), [4, 5]),)) Traceback (most recent call last): ... - ValueError: No possible values found for singleton block [[1, 2]] + ValueError: no possible values found for singleton block [[1, 2]] """ # **************************************************************************** @@ -392,9 +408,8 @@ class Bijectionist(SageObject): - ``pi_rho`` -- (optional) a list of triples ``(k, pi, rho)``, where - - ``pi`` -- a ``k``-ary operation composing objects in ``A`` and - - - ``rho`` -- a ``k``-ary function composing statistic values in `Z` + * ``pi`` -- a ``k``-ary operation composing objects in ``A`` and + * ``rho`` -- a ``k``-ary function composing statistic values in ``Z`` - ``elements_distributions`` -- (optional) a list of pairs ``(tA, tZ)``, specifying the distributions of ``tA`` @@ -468,7 +483,7 @@ class Bijectionist(SageObject): methods a second time overrides the previous specification. """ - def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], + def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=None, pi_rho=tuple(), phi_psi=tuple(), Q=None, elements_distributions=tuple(), value_restrictions=tuple(), solver=None, key=None): @@ -518,6 +533,8 @@ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=[], except TypeError: self._Z = list(self._Z) self._sorter["Z"] = lambda x: list(x) + if P is None: + P = [] # set optional inputs self.set_statistics(*alpha_beta) @@ -701,7 +718,7 @@ def set_statistics(self, *alpha_beta): sage: bij.set_statistics((wex, fix)) Traceback (most recent call last): ... - ValueError: Statistics alpha and beta are not equidistributed! + ValueError: statistics alpha and beta are not equidistributed TESTS: @@ -750,13 +767,13 @@ def set_statistics(self, *alpha_beta): for b in self._B: v = self._beta(b) if v not in self._statistics_fibers: - raise ValueError(f"Statistics alpha and beta do not have the same image, {v} is not a value of alpha, but of beta!") + raise ValueError(f"statistics alpha and beta do not have the same image, {v} is not a value of alpha, but of beta") self._statistics_fibers[v][1].append(b) # check compatibility if not all(len(fiber[0]) == len(fiber[1]) for fiber in self._statistics_fibers.values()): - raise ValueError("Statistics alpha and beta are not equidistributed!") + raise ValueError("statistics alpha and beta are not equidistributed") self._W = list(self._statistics_fibers) @@ -810,8 +827,8 @@ def statistics_table(self, header=True): INPUT: - - ``header`` -- (optional, default: ``True``) whether to include a - header with the standard greek letters. + - ``header`` -- (default: ``True``) whether to include a + header with the standard Greek letters OUTPUT: @@ -1023,7 +1040,7 @@ def set_value_restrictions(self, *value_restrictions): sage: bij._compute_possible_block_values() Traceback (most recent call last): ... - ValueError: No possible values found for singleton block [[1, 2]] + ValueError: no possible values found for singleton block [[1, 2]] sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] sage: tau = Permutation.longest_increasing_subsequence_length @@ -1033,7 +1050,7 @@ def set_value_restrictions(self, *value_restrictions): sage: bij._compute_possible_block_values() Traceback (most recent call last): ... - ValueError: No possible values found for block [[1, 2], [2, 1]] + ValueError: no possible values found for block [[1, 2], [2, 1]] sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)] sage: tau = Permutation.longest_increasing_subsequence_length @@ -1041,7 +1058,7 @@ def set_value_restrictions(self, *value_restrictions): sage: bij.set_value_restrictions(((1, 2), [4, 5, 6])) Traceback (most recent call last): ... - AssertionError: Element (1, 2) was not found in A + AssertionError: element (1, 2) was not found in A """ # it might be much cheaper to construct the sets as subsets @@ -1052,7 +1069,7 @@ def set_value_restrictions(self, *value_restrictions): set_Z = set(self._Z) self._restrictions_possible_values = {a: set_Z for a in self._A} for a, values in value_restrictions: - assert a in self._A, f"Element {a} was not found in A" + assert a in self._A, f"element {a} was not found in A" self._restrictions_possible_values[a] = self._restrictions_possible_values[a].intersection(values) def _compute_possible_block_values(self): @@ -1073,7 +1090,7 @@ def _compute_possible_block_values(self): sage: bij._compute_possible_block_values() Traceback (most recent call last): ... - ValueError: No possible values found for singleton block [[1, 2]] + ValueError: no possible values found for singleton block [[1, 2]] """ self._possible_block_values = {} # P -> Power(Z) @@ -1083,9 +1100,9 @@ def _compute_possible_block_values(self): self._possible_block_values[p] = _non_copying_intersection(sets) if not self._possible_block_values[p]: if len(block) == 1: - raise ValueError(f"No possible values found for singleton block {block}") + raise ValueError(f"no possible values found for singleton block {block}") else: - raise ValueError(f"No possible values found for block {block}") + raise ValueError(f"no possible values found for block {block}") def set_distributions(self, *elements_distributions): r""" @@ -1229,11 +1246,11 @@ def set_distributions(self, *elements_distributions): sage: bij.set_distributions(([Permutation([1, 2, 3, 4])], [1])) Traceback (most recent call last): ... - ValueError: Element [1, 2, 3, 4] was not found in A! + ValueError: element [1, 2, 3, 4] was not found in A sage: bij.set_distributions(([Permutation([1, 2, 3])], [-1])) Traceback (most recent call last): ... - ValueError: Value -1 was not found in tau(A)! + ValueError: value -1 was not found in tau(A) Note that the same error occurs when an element that is not the first element of the list is not in `A`. @@ -1244,9 +1261,9 @@ def set_distributions(self, *elements_distributions): assert len(tA) == len(tZ), f"{tA} and {tZ} are not of the same size!" for a, z in zip(tA, tZ): if a not in self._A: - raise ValueError(f"Element {a} was not found in A!") + raise ValueError(f"element {a} was not found in A") if z not in self._Z: - raise ValueError(f"Value {z} was not found in tau(A)!") + raise ValueError(f"value {z} was not found in tau(A)") self._elements_distributions = tuple(elements_distributions) def set_intertwining_relations(self, *pi_rho): @@ -1442,7 +1459,7 @@ def set_homomesic(self, Q): INPUT: - - ``Q`` -- a set partition of ``A``. + - ``Q`` -- a set partition of ``A`` EXAMPLES:: @@ -1698,15 +1715,15 @@ def possible_values(self, p=None, optimal=False): INPUT: - - ``p`` -- (optional, default: ``None``) -- a block of `P`, or - an element of a block of `P`, or a list of these + - ``p`` -- (optional) a block of `P`, or an element of a + block of `P`, or a list of these - - ``optimal`` -- (optional, default: ``False``) whether or - not to compute the minimal possible set of statistic values. + - ``optimal`` -- (default: ``False``) whether or not to + compute the minimal possible set of statistic values .. NOTE:: - computing the minimal possible set of statistic values + Computing the minimal possible set of statistic values may be computationally expensive. .. TODO:: @@ -1758,6 +1775,7 @@ def possible_values(self, p=None, optimal=False): {'a': {0, 1}, 'b': {0, 1}} sage: bij.possible_values(p="a", optimal=True) {'a': set(), 'b': set()} + """ # convert input to set of block representatives blocks = set() @@ -1896,15 +1914,16 @@ def _find_counterexample(self, P, s0, d, on_blocks): INPUT: - - ``P``, the representatives of the blocks, or `A` if - ``on_blocks`` is ``False``. + - ``P`` -- the representatives of the blocks, or `A` if + ``on_blocks`` is ``False`` - - ``s0``, a solution. + - ``s0`` -- a solution - - ``d``, a subset of `A`, in the form of a dict from `A` to `\{0, 1\}`. + - ``d`` -- a subset of `A`, in the form of a dict from `A` to + `\{0, 1\}` - - ``on_blocks``, whether to return the counter example on - blocks or on elements. + - ``on_blocks`` -- whether to return the counterexample on + blocks or on elements EXAMPLES:: @@ -1920,6 +1939,7 @@ def _find_counterexample(self, P, s0, d, on_blocks): sage: d = {'a': 1, 'b': 0, 'c': 0, 'd': 0, 'e': 0} sage: bij._find_counterexample(bij._A, s0, d, False) {'a': 2, 'b': 2, 'c': 1, 'd': 3, 'e': 1} + """ bmilp = self._bmilp for z in self._Z: @@ -2446,8 +2466,9 @@ def solutions_iterator(self): class _BijectionistMILP(): r""" - Wrapper class for the MixedIntegerLinearProgram (MILP). This - class is used to manage the MILP, add constraints, solve the + Wrapper class for the MixedIntegerLinearProgram (MILP). + + This class is used to manage the MILP, add constraints, solve the problem and check for uniqueness of solution values. """ def __init__(self, bijectionist: Bijectionist, solutions=None): @@ -2458,10 +2479,10 @@ def __init__(self, bijectionist: Bijectionist, solutions=None): - ``bijectionist`` -- an instance of :class:`Bijectionist`. - - ``solutions`` -- (optional, default: ``None``) a list of solutions of - the problem, each provided as a dictionary mapping `(a, z)` to a - Boolean, such that at least one element from each block of `P` - appears as `a`. + - ``solutions`` -- (optional) a list of solutions of the + problem, each provided as a dictionary mapping `(a, z)` to + a boolean, such that at least one element from each block + of `P` appears as `a`. .. TODO:: @@ -2585,8 +2606,8 @@ def _prepare_solution(self, on_blocks, solution): INPUT: - - ``on_blocks``, whether to return the solution on blocks or - on all elements + - ``on_blocks`` -- whether to return the solution on blocks + or on all elements TESTS:: @@ -2621,10 +2642,10 @@ def solutions_iterator(self, on_blocks, additional_constraints): INPUT: - ``additional_constraints`` -- a list of constraints for the - underlying MILP. + underlying MILP - ``on_blocks``, whether to return the solution on blocks or - on all elements. + on all elements TESTS:: @@ -2689,8 +2710,8 @@ def _add_solution(self, solution): INPUT: - - ``solution``, a dictionary from the indices of the MILP to - Boolean. + - ``solution`` -- a dictionary from the indices of the MILP to + a boolean EXAMPLES:: @@ -2727,10 +2748,10 @@ def _is_solution(self, constraint, values): INPUT: - - ``constraint``, a + - ``constraint`` -- a :class:`sage.numerical.linear_functions.LinearConstraint`. - - ``values``, a candidate for a solution of the MILP as a + - ``values`` -- a candidate for a solution of the MILP as a dictionary from pairs `(a, z)\in A\times Z` to `0` or `1`, specifying whether `a` is mapped to `z`. @@ -2750,7 +2771,7 @@ def _is_solution(self, constraint, values): """ index_block_value_dict = {} for (p, z), v in self._x.items(): - variable_index = next(iter(v.dict().keys())) + variable_index = next(iter(v.dict())) index_block_value_dict[variable_index] = (p, z) def evaluate(f): @@ -2793,8 +2814,8 @@ def add_alpha_beta_constraints(self): W = self._bijectionist._W Z = self._bijectionist._Z zero = self.milp.linear_functions_parent().zero() - AZ_matrix = [[zero]*len(W) for _ in range(len(Z))] - B_matrix = [[zero]*len(W) for _ in range(len(Z))] + AZ_matrix = [[zero] * len(W) for _ in range(len(Z))] + B_matrix = [[zero] * len(W) for _ in range(len(Z))] W_dict = {w: i for i, w in enumerate(W)} Z_dict = {z: i for i, z in enumerate(Z)} @@ -2922,7 +2943,7 @@ def add_intertwining_relation_constraints(self): P = self._bijectionist._P for composition_index, pi_rho in enumerate(self._bijectionist._pi_rho): pi_blocks = set() - for a_tuple in itertools.product(*([A]*pi_rho.numargs)): + for a_tuple in itertools.product(A, repeat=pi_rho.numargs): if pi_rho.domain is not None and not pi_rho.domain(*a_tuple): continue a = pi_rho.pi(*a_tuple) @@ -3048,7 +3069,7 @@ def _invert_dict(d): INPUT: - - ``d``, a ``dict``. + - ``d`` -- a dict EXAMPLES:: @@ -3071,7 +3092,7 @@ def _disjoint_set_roots(d): INPUT: - - ``d``, a ``sage.sets.disjoint_set.DisjointSet_of_hashables`` + - ``d`` -- a :class:`sage.sets.disjoint_set.DisjointSet_of_hashables` EXAMPLES:: @@ -3125,7 +3146,7 @@ def _non_copying_intersection(sets): ....: [(3,i,j) for i in [-2,-1,0,1,2] for j in [-1,1]]] Note that adding ``[(2,-2,-1), (2,2,-1), (2,-2,1), (2,2,1)]`` makes -it take (seemingly) forever.:: +it take (seemingly) forever:: sage: def c1(a, b): return (a[0]+b[0], a[1]*b[1], a[2]*b[2]) sage: def c2(a): return (a[0], -a[1], a[2]) From f87c43702ddbcb5ad221fcb5a8bf9b7a57fcbfb9 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Sat, 11 Mar 2023 16:11:40 +0100 Subject: [PATCH 51/51] sort result of doctest to avoid random failures --- src/sage/combinat/bijectionist.py | 55 +++++++++++++------------------ 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/src/sage/combinat/bijectionist.py b/src/sage/combinat/bijectionist.py index 8844f4f3602..0c7d50a7659 100644 --- a/src/sage/combinat/bijectionist.py +++ b/src/sage/combinat/bijectionist.py @@ -1575,41 +1575,32 @@ def _forced_constant_blocks(self): sage: bij.set_statistics((alpha1, beta1), (alpha2, beta2)) sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition sage: bij.set_constant_blocks(orbit_decomposition(A, rotate_permutation)) - sage: for p in bij.constant_blocks(): print(list(p)) - [[2, 1, 3, 4], [1, 2, 4, 3], [1, 3, 2, 4], [4, 2, 3, 1]] - [[3, 2, 1], [1, 3, 2], [2, 1, 3]] - [[2, 4, 3, 1], [3, 2, 4, 1], [2, 3, 1, 4], [1, 3, 4, 2]] - [[1, 4, 2, 3], [3, 1, 2, 4], [4, 2, 1, 3], [4, 1, 3, 2]] + sage: P = bij.constant_blocks() + sage: P = [sorted(p, key=lambda p: (len(p), p)) for p in P] + sage: P = sorted(P, key=lambda p: (len(next(iter(p))), len(p))) + sage: for p in P: + ....: print(p) + [[1, 3, 2], [2, 1, 3], [3, 2, 1]] [[1, 4, 3, 2], [3, 2, 1, 4]] [[2, 1, 4, 3], [4, 3, 2, 1]] - [[2, 4, 1, 3], [3, 4, 2, 1], [4, 3, 1, 2], [3, 1, 4, 2]] - - sage: for p in bij.constant_blocks(optimal=True): sorted(p, key=len) + [[1, 2, 4, 3], [1, 3, 2, 4], [2, 1, 3, 4], [4, 2, 3, 1]] + [[1, 3, 4, 2], [2, 3, 1, 4], [2, 4, 3, 1], [3, 2, 4, 1]] + [[1, 4, 2, 3], [3, 1, 2, 4], [4, 1, 3, 2], [4, 2, 1, 3]] + [[2, 4, 1, 3], [3, 1, 4, 2], [3, 4, 2, 1], [4, 3, 1, 2]] + + sage: P = bij.constant_blocks(optimal=True) + sage: P = [sorted(p, key=lambda p: (len(p), p)) for p in P] + sage: P = sorted(P, key=lambda p: (len(next(iter(p))), len(p))) + sage: for p in P: + ....: print(p) [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4]] - [[1, 3, 2], - [2, 1, 3], - [3, 2, 1], - [2, 3, 4, 1], - [1, 3, 4, 2], - [2, 1, 3, 4], - [1, 3, 2, 4], - [2, 3, 1, 4], - [1, 2, 4, 3], - [3, 2, 4, 1], - [2, 1, 4, 3], - [2, 4, 3, 1], - [4, 2, 3, 1], - [4, 3, 2, 1], - [1, 4, 3, 2], - [3, 2, 1, 4]] - [[1, 4, 2, 3], - [4, 2, 1, 3], - [2, 4, 1, 3], - [4, 3, 1, 2], - [4, 1, 3, 2], - [3, 4, 2, 1], - [3, 1, 2, 4], - [3, 1, 4, 2]] + [[1, 3, 2], [2, 1, 3], [3, 2, 1], + [1, 2, 4, 3], [1, 3, 2, 4], [1, 3, 4, 2], [1, 4, 3, 2], + [2, 1, 3, 4], [2, 1, 4, 3], [2, 3, 1, 4], [2, 3, 4, 1], + [2, 4, 3, 1], [3, 2, 1, 4], [3, 2, 4, 1], [4, 2, 3, 1], + [4, 3, 2, 1]] + [[1, 4, 2, 3], [2, 4, 1, 3], [3, 1, 2, 4], [3, 1, 4, 2], + [3, 4, 2, 1], [4, 1, 3, 2], [4, 2, 1, 3], [4, 3, 1, 2]] The permutation `[2, 1]` is in none of these blocks::