passagemath-combinat 10.6.42__cp314-cp314-musllinux_1_2_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (400) hide show
  1. passagemath_combinat/__init__.py +3 -0
  2. passagemath_combinat-10.6.42.dist-info/METADATA +160 -0
  3. passagemath_combinat-10.6.42.dist-info/RECORD +400 -0
  4. passagemath_combinat-10.6.42.dist-info/WHEEL +5 -0
  5. passagemath_combinat-10.6.42.dist-info/top_level.txt +3 -0
  6. passagemath_combinat.libs/libgmp-0e7fc84e.so.10.5.0 +0 -0
  7. passagemath_combinat.libs/libsymmetrica-81fe8739.so.3.0.0 +0 -0
  8. sage/algebras/affine_nil_temperley_lieb.py +263 -0
  9. sage/algebras/all.py +24 -0
  10. sage/algebras/all__sagemath_combinat.py +35 -0
  11. sage/algebras/askey_wilson.py +935 -0
  12. sage/algebras/associated_graded.py +345 -0
  13. sage/algebras/cellular_basis.py +350 -0
  14. sage/algebras/cluster_algebra.py +2766 -0
  15. sage/algebras/down_up_algebra.py +860 -0
  16. sage/algebras/free_algebra.py +1698 -0
  17. sage/algebras/free_algebra_element.py +345 -0
  18. sage/algebras/free_algebra_quotient.py +405 -0
  19. sage/algebras/free_algebra_quotient_element.py +295 -0
  20. sage/algebras/free_zinbiel_algebra.py +885 -0
  21. sage/algebras/hall_algebra.py +783 -0
  22. sage/algebras/hecke_algebras/all.py +4 -0
  23. sage/algebras/hecke_algebras/ariki_koike_algebra.py +1796 -0
  24. sage/algebras/hecke_algebras/ariki_koike_specht_modules.py +475 -0
  25. sage/algebras/hecke_algebras/cubic_hecke_algebra.py +3520 -0
  26. sage/algebras/hecke_algebras/cubic_hecke_base_ring.py +1473 -0
  27. sage/algebras/hecke_algebras/cubic_hecke_matrix_rep.py +1079 -0
  28. sage/algebras/iwahori_hecke_algebra.py +3095 -0
  29. sage/algebras/jordan_algebra.py +1773 -0
  30. sage/algebras/lie_conformal_algebras/abelian_lie_conformal_algebra.py +113 -0
  31. sage/algebras/lie_conformal_algebras/affine_lie_conformal_algebra.py +156 -0
  32. sage/algebras/lie_conformal_algebras/all.py +18 -0
  33. sage/algebras/lie_conformal_algebras/bosonic_ghosts_lie_conformal_algebra.py +134 -0
  34. sage/algebras/lie_conformal_algebras/examples.py +43 -0
  35. sage/algebras/lie_conformal_algebras/fermionic_ghosts_lie_conformal_algebra.py +131 -0
  36. sage/algebras/lie_conformal_algebras/finitely_freely_generated_lca.py +139 -0
  37. sage/algebras/lie_conformal_algebras/free_bosons_lie_conformal_algebra.py +174 -0
  38. sage/algebras/lie_conformal_algebras/free_fermions_lie_conformal_algebra.py +167 -0
  39. sage/algebras/lie_conformal_algebras/freely_generated_lie_conformal_algebra.py +107 -0
  40. sage/algebras/lie_conformal_algebras/graded_lie_conformal_algebra.py +135 -0
  41. sage/algebras/lie_conformal_algebras/lie_conformal_algebra.py +353 -0
  42. sage/algebras/lie_conformal_algebras/lie_conformal_algebra_element.py +236 -0
  43. sage/algebras/lie_conformal_algebras/lie_conformal_algebra_with_basis.py +78 -0
  44. sage/algebras/lie_conformal_algebras/lie_conformal_algebra_with_structure_coefs.py +328 -0
  45. sage/algebras/lie_conformal_algebras/n2_lie_conformal_algebra.py +117 -0
  46. sage/algebras/lie_conformal_algebras/neveu_schwarz_lie_conformal_algebra.py +86 -0
  47. sage/algebras/lie_conformal_algebras/virasoro_lie_conformal_algebra.py +82 -0
  48. sage/algebras/lie_conformal_algebras/weyl_lie_conformal_algebra.py +205 -0
  49. sage/algebras/nil_coxeter_algebra.py +191 -0
  50. sage/algebras/q_commuting_polynomials.py +673 -0
  51. sage/algebras/q_system.py +608 -0
  52. sage/algebras/quantum_clifford.py +959 -0
  53. sage/algebras/quantum_groups/ace_quantum_onsager.py +693 -0
  54. sage/algebras/quantum_groups/all.py +9 -0
  55. sage/algebras/quantum_groups/fock_space.py +2219 -0
  56. sage/algebras/quantum_groups/q_numbers.py +207 -0
  57. sage/algebras/quantum_groups/quantum_group_gap.py +2695 -0
  58. sage/algebras/quantum_groups/representations.py +591 -0
  59. sage/algebras/quantum_matrix_coordinate_algebra.py +1006 -0
  60. sage/algebras/quantum_oscillator.py +623 -0
  61. sage/algebras/quaternion_algebra.py +20 -0
  62. sage/algebras/quaternion_algebra_element.py +55 -0
  63. sage/algebras/rational_cherednik_algebra.py +525 -0
  64. sage/algebras/schur_algebra.py +670 -0
  65. sage/algebras/shuffle_algebra.py +1011 -0
  66. sage/algebras/splitting_algebra.py +779 -0
  67. sage/algebras/tensor_algebra.py +709 -0
  68. sage/algebras/yangian.py +1082 -0
  69. sage/algebras/yokonuma_hecke_algebra.py +1018 -0
  70. sage/all__sagemath_combinat.py +35 -0
  71. sage/combinat/SJT.py +255 -0
  72. sage/combinat/affine_permutation.py +2405 -0
  73. sage/combinat/algebraic_combinatorics.py +55 -0
  74. sage/combinat/all.py +53 -0
  75. sage/combinat/all__sagemath_combinat.py +195 -0
  76. sage/combinat/alternating_sign_matrix.py +2063 -0
  77. sage/combinat/baxter_permutations.py +346 -0
  78. sage/combinat/bijectionist.py +3220 -0
  79. sage/combinat/binary_recurrence_sequences.py +1180 -0
  80. sage/combinat/blob_algebra.py +685 -0
  81. sage/combinat/catalog_partitions.py +27 -0
  82. sage/combinat/chas/all.py +23 -0
  83. sage/combinat/chas/fsym.py +1180 -0
  84. sage/combinat/chas/wqsym.py +2601 -0
  85. sage/combinat/cluster_complex.py +326 -0
  86. sage/combinat/colored_permutations.py +2039 -0
  87. sage/combinat/colored_permutations_representations.py +964 -0
  88. sage/combinat/composition_signed.py +142 -0
  89. sage/combinat/composition_tableau.py +855 -0
  90. sage/combinat/constellation.py +1729 -0
  91. sage/combinat/core.py +751 -0
  92. sage/combinat/counting.py +12 -0
  93. sage/combinat/crystals/affine.py +742 -0
  94. sage/combinat/crystals/affine_factorization.py +518 -0
  95. sage/combinat/crystals/affinization.py +331 -0
  96. sage/combinat/crystals/alcove_path.py +2013 -0
  97. sage/combinat/crystals/all.py +22 -0
  98. sage/combinat/crystals/bkk_crystals.py +141 -0
  99. sage/combinat/crystals/catalog.py +115 -0
  100. sage/combinat/crystals/catalog_elementary_crystals.py +18 -0
  101. sage/combinat/crystals/catalog_infinity_crystals.py +33 -0
  102. sage/combinat/crystals/catalog_kirillov_reshetikhin.py +18 -0
  103. sage/combinat/crystals/crystals.py +257 -0
  104. sage/combinat/crystals/direct_sum.py +260 -0
  105. sage/combinat/crystals/elementary_crystals.py +1251 -0
  106. sage/combinat/crystals/fast_crystals.py +441 -0
  107. sage/combinat/crystals/fully_commutative_stable_grothendieck.py +1205 -0
  108. sage/combinat/crystals/generalized_young_walls.py +1076 -0
  109. sage/combinat/crystals/highest_weight_crystals.py +436 -0
  110. sage/combinat/crystals/induced_structure.py +695 -0
  111. sage/combinat/crystals/infinity_crystals.py +730 -0
  112. sage/combinat/crystals/kac_modules.py +863 -0
  113. sage/combinat/crystals/kirillov_reshetikhin.py +4196 -0
  114. sage/combinat/crystals/kyoto_path_model.py +497 -0
  115. sage/combinat/crystals/letters.cpython-314-x86_64-linux-musl.so +0 -0
  116. sage/combinat/crystals/letters.pxd +79 -0
  117. sage/combinat/crystals/letters.pyx +3056 -0
  118. sage/combinat/crystals/littelmann_path.py +1518 -0
  119. sage/combinat/crystals/monomial_crystals.py +1262 -0
  120. sage/combinat/crystals/multisegments.py +462 -0
  121. sage/combinat/crystals/mv_polytopes.py +467 -0
  122. sage/combinat/crystals/pbw_crystal.py +511 -0
  123. sage/combinat/crystals/pbw_datum.cpython-314-x86_64-linux-musl.so +0 -0
  124. sage/combinat/crystals/pbw_datum.pxd +4 -0
  125. sage/combinat/crystals/pbw_datum.pyx +487 -0
  126. sage/combinat/crystals/polyhedral_realization.py +372 -0
  127. sage/combinat/crystals/spins.cpython-314-x86_64-linux-musl.so +0 -0
  128. sage/combinat/crystals/spins.pxd +21 -0
  129. sage/combinat/crystals/spins.pyx +756 -0
  130. sage/combinat/crystals/star_crystal.py +290 -0
  131. sage/combinat/crystals/subcrystal.py +464 -0
  132. sage/combinat/crystals/tensor_product.py +1177 -0
  133. sage/combinat/crystals/tensor_product_element.cpython-314-x86_64-linux-musl.so +0 -0
  134. sage/combinat/crystals/tensor_product_element.pxd +35 -0
  135. sage/combinat/crystals/tensor_product_element.pyx +1870 -0
  136. sage/combinat/crystals/virtual_crystal.py +420 -0
  137. sage/combinat/cyclic_sieving_phenomenon.py +204 -0
  138. sage/combinat/debruijn_sequence.cpython-314-x86_64-linux-musl.so +0 -0
  139. sage/combinat/debruijn_sequence.pyx +355 -0
  140. sage/combinat/decorated_permutation.py +270 -0
  141. sage/combinat/degree_sequences.cpython-314-x86_64-linux-musl.so +0 -0
  142. sage/combinat/degree_sequences.pyx +588 -0
  143. sage/combinat/derangements.py +527 -0
  144. sage/combinat/descent_algebra.py +1008 -0
  145. sage/combinat/diagram.py +1551 -0
  146. sage/combinat/diagram_algebras.py +5886 -0
  147. sage/combinat/dyck_word.py +4349 -0
  148. sage/combinat/e_one_star.py +1623 -0
  149. sage/combinat/enumerated_sets.py +123 -0
  150. sage/combinat/expnums.cpython-314-x86_64-linux-musl.so +0 -0
  151. sage/combinat/expnums.pyx +148 -0
  152. sage/combinat/fast_vector_partitions.cpython-314-x86_64-linux-musl.so +0 -0
  153. sage/combinat/fast_vector_partitions.pyx +346 -0
  154. sage/combinat/fqsym.py +1977 -0
  155. sage/combinat/free_dendriform_algebra.py +954 -0
  156. sage/combinat/free_prelie_algebra.py +1141 -0
  157. sage/combinat/fully_commutative_elements.py +1077 -0
  158. sage/combinat/fully_packed_loop.py +1523 -0
  159. sage/combinat/gelfand_tsetlin_patterns.py +1409 -0
  160. sage/combinat/gray_codes.py +311 -0
  161. sage/combinat/grossman_larson_algebras.py +667 -0
  162. sage/combinat/growth.py +4352 -0
  163. sage/combinat/hall_polynomial.py +188 -0
  164. sage/combinat/hillman_grassl.py +866 -0
  165. sage/combinat/integer_matrices.py +329 -0
  166. sage/combinat/integer_vectors_mod_permgroup.py +1238 -0
  167. sage/combinat/k_tableau.py +4564 -0
  168. sage/combinat/kazhdan_lusztig.py +215 -0
  169. sage/combinat/key_polynomial.py +885 -0
  170. sage/combinat/knutson_tao_puzzles.py +2286 -0
  171. sage/combinat/lr_tableau.py +311 -0
  172. sage/combinat/matrices/all.py +24 -0
  173. sage/combinat/matrices/hadamard_matrix.py +3790 -0
  174. sage/combinat/matrices/latin.py +2912 -0
  175. sage/combinat/misc.py +401 -0
  176. sage/combinat/multiset_partition_into_sets_ordered.py +3541 -0
  177. sage/combinat/ncsf_qsym/all.py +21 -0
  178. sage/combinat/ncsf_qsym/combinatorics.py +317 -0
  179. sage/combinat/ncsf_qsym/generic_basis_code.py +1427 -0
  180. sage/combinat/ncsf_qsym/ncsf.py +5637 -0
  181. sage/combinat/ncsf_qsym/qsym.py +4053 -0
  182. sage/combinat/ncsf_qsym/tutorial.py +447 -0
  183. sage/combinat/ncsym/all.py +21 -0
  184. sage/combinat/ncsym/bases.py +855 -0
  185. sage/combinat/ncsym/dual.py +593 -0
  186. sage/combinat/ncsym/ncsym.py +2076 -0
  187. sage/combinat/necklace.py +551 -0
  188. sage/combinat/non_decreasing_parking_function.py +634 -0
  189. sage/combinat/nu_dyck_word.py +1474 -0
  190. sage/combinat/output.py +861 -0
  191. sage/combinat/parallelogram_polyomino.py +4326 -0
  192. sage/combinat/parking_functions.py +1602 -0
  193. sage/combinat/partition_algebra.py +1998 -0
  194. sage/combinat/partition_kleshchev.py +1982 -0
  195. sage/combinat/partition_shifting_algebras.py +584 -0
  196. sage/combinat/partition_tuple.py +3114 -0
  197. sage/combinat/path_tableaux/all.py +13 -0
  198. sage/combinat/path_tableaux/catalog.py +29 -0
  199. sage/combinat/path_tableaux/dyck_path.py +380 -0
  200. sage/combinat/path_tableaux/frieze.py +476 -0
  201. sage/combinat/path_tableaux/path_tableau.py +728 -0
  202. sage/combinat/path_tableaux/semistandard.py +510 -0
  203. sage/combinat/perfect_matching.py +779 -0
  204. sage/combinat/plane_partition.py +3300 -0
  205. sage/combinat/q_bernoulli.cpython-314-x86_64-linux-musl.so +0 -0
  206. sage/combinat/q_bernoulli.pyx +128 -0
  207. sage/combinat/quickref.py +81 -0
  208. sage/combinat/recognizable_series.py +2051 -0
  209. sage/combinat/regular_sequence.py +4316 -0
  210. sage/combinat/regular_sequence_bounded.py +543 -0
  211. sage/combinat/restricted_growth.py +81 -0
  212. sage/combinat/ribbon.py +20 -0
  213. sage/combinat/ribbon_shaped_tableau.py +489 -0
  214. sage/combinat/ribbon_tableau.py +1180 -0
  215. sage/combinat/rigged_configurations/all.py +46 -0
  216. sage/combinat/rigged_configurations/bij_abstract_class.py +548 -0
  217. sage/combinat/rigged_configurations/bij_infinity.py +370 -0
  218. sage/combinat/rigged_configurations/bij_type_A.py +163 -0
  219. sage/combinat/rigged_configurations/bij_type_A2_dual.py +338 -0
  220. sage/combinat/rigged_configurations/bij_type_A2_even.py +218 -0
  221. sage/combinat/rigged_configurations/bij_type_A2_odd.py +199 -0
  222. sage/combinat/rigged_configurations/bij_type_B.py +900 -0
  223. sage/combinat/rigged_configurations/bij_type_C.py +267 -0
  224. sage/combinat/rigged_configurations/bij_type_D.py +771 -0
  225. sage/combinat/rigged_configurations/bij_type_D_tri.py +392 -0
  226. sage/combinat/rigged_configurations/bij_type_D_twisted.py +576 -0
  227. sage/combinat/rigged_configurations/bij_type_E67.py +402 -0
  228. sage/combinat/rigged_configurations/bijection.py +143 -0
  229. sage/combinat/rigged_configurations/kleber_tree.py +1475 -0
  230. sage/combinat/rigged_configurations/kr_tableaux.py +1898 -0
  231. sage/combinat/rigged_configurations/rc_crystal.py +461 -0
  232. sage/combinat/rigged_configurations/rc_infinity.py +540 -0
  233. sage/combinat/rigged_configurations/rigged_configuration_element.py +2403 -0
  234. sage/combinat/rigged_configurations/rigged_configurations.py +1918 -0
  235. sage/combinat/rigged_configurations/rigged_partition.cpython-314-x86_64-linux-musl.so +0 -0
  236. sage/combinat/rigged_configurations/rigged_partition.pxd +15 -0
  237. sage/combinat/rigged_configurations/rigged_partition.pyx +680 -0
  238. sage/combinat/rigged_configurations/tensor_product_kr_tableaux.py +499 -0
  239. sage/combinat/rigged_configurations/tensor_product_kr_tableaux_element.py +428 -0
  240. sage/combinat/rsk.py +3438 -0
  241. sage/combinat/schubert_polynomial.py +508 -0
  242. sage/combinat/set_partition.py +3318 -0
  243. sage/combinat/set_partition_iterator.cpython-314-x86_64-linux-musl.so +0 -0
  244. sage/combinat/set_partition_iterator.pyx +136 -0
  245. sage/combinat/set_partition_ordered.py +1590 -0
  246. sage/combinat/sf/abreu_nigro.py +346 -0
  247. sage/combinat/sf/all.py +52 -0
  248. sage/combinat/sf/character.py +576 -0
  249. sage/combinat/sf/classical.py +319 -0
  250. sage/combinat/sf/dual.py +996 -0
  251. sage/combinat/sf/elementary.py +549 -0
  252. sage/combinat/sf/hall_littlewood.py +1028 -0
  253. sage/combinat/sf/hecke.py +336 -0
  254. sage/combinat/sf/homogeneous.py +464 -0
  255. sage/combinat/sf/jack.py +1428 -0
  256. sage/combinat/sf/k_dual.py +1458 -0
  257. sage/combinat/sf/kfpoly.py +447 -0
  258. sage/combinat/sf/llt.py +789 -0
  259. sage/combinat/sf/macdonald.py +2019 -0
  260. sage/combinat/sf/monomial.py +525 -0
  261. sage/combinat/sf/multiplicative.py +113 -0
  262. sage/combinat/sf/new_kschur.py +1786 -0
  263. sage/combinat/sf/ns_macdonald.py +964 -0
  264. sage/combinat/sf/orthogonal.py +246 -0
  265. sage/combinat/sf/orthotriang.py +355 -0
  266. sage/combinat/sf/powersum.py +963 -0
  267. sage/combinat/sf/schur.py +880 -0
  268. sage/combinat/sf/sf.py +1653 -0
  269. sage/combinat/sf/sfa.py +7053 -0
  270. sage/combinat/sf/symplectic.py +253 -0
  271. sage/combinat/sf/witt.py +721 -0
  272. sage/combinat/shifted_primed_tableau.py +2735 -0
  273. sage/combinat/shuffle.py +830 -0
  274. sage/combinat/sidon_sets.py +146 -0
  275. sage/combinat/similarity_class_type.py +1721 -0
  276. sage/combinat/sine_gordon.py +618 -0
  277. sage/combinat/six_vertex_model.py +784 -0
  278. sage/combinat/skew_partition.py +2053 -0
  279. sage/combinat/skew_tableau.py +2989 -0
  280. sage/combinat/sloane_functions.py +8935 -0
  281. sage/combinat/specht_module.py +1403 -0
  282. sage/combinat/species/all.py +48 -0
  283. sage/combinat/species/characteristic_species.py +321 -0
  284. sage/combinat/species/composition_species.py +273 -0
  285. sage/combinat/species/cycle_species.py +284 -0
  286. sage/combinat/species/empty_species.py +155 -0
  287. sage/combinat/species/functorial_composition_species.py +148 -0
  288. sage/combinat/species/generating_series.py +673 -0
  289. sage/combinat/species/library.py +148 -0
  290. sage/combinat/species/linear_order_species.py +169 -0
  291. sage/combinat/species/misc.py +83 -0
  292. sage/combinat/species/partition_species.py +290 -0
  293. sage/combinat/species/permutation_species.py +268 -0
  294. sage/combinat/species/product_species.py +423 -0
  295. sage/combinat/species/recursive_species.py +476 -0
  296. sage/combinat/species/set_species.py +192 -0
  297. sage/combinat/species/species.py +820 -0
  298. sage/combinat/species/structure.py +539 -0
  299. sage/combinat/species/subset_species.py +243 -0
  300. sage/combinat/species/sum_species.py +225 -0
  301. sage/combinat/subword.py +564 -0
  302. sage/combinat/subword_complex.py +2122 -0
  303. sage/combinat/subword_complex_c.cpython-314-x86_64-linux-musl.so +0 -0
  304. sage/combinat/subword_complex_c.pyx +119 -0
  305. sage/combinat/super_tableau.py +821 -0
  306. sage/combinat/superpartition.py +1154 -0
  307. sage/combinat/symmetric_group_algebra.py +3774 -0
  308. sage/combinat/symmetric_group_representations.py +1830 -0
  309. sage/combinat/t_sequences.py +877 -0
  310. sage/combinat/tableau.py +9506 -0
  311. sage/combinat/tableau_residues.py +860 -0
  312. sage/combinat/tableau_tuple.py +5353 -0
  313. sage/combinat/tiling.py +2432 -0
  314. sage/combinat/triangles_FHM.py +777 -0
  315. sage/combinat/tutorial.py +1857 -0
  316. sage/combinat/vector_partition.py +337 -0
  317. sage/combinat/words/abstract_word.py +1722 -0
  318. sage/combinat/words/all.py +59 -0
  319. sage/combinat/words/alphabet.py +268 -0
  320. sage/combinat/words/finite_word.py +7201 -0
  321. sage/combinat/words/infinite_word.py +113 -0
  322. sage/combinat/words/lyndon_word.py +652 -0
  323. sage/combinat/words/morphic.py +351 -0
  324. sage/combinat/words/morphism.py +3878 -0
  325. sage/combinat/words/paths.py +2932 -0
  326. sage/combinat/words/shuffle_product.py +278 -0
  327. sage/combinat/words/suffix_trees.py +1873 -0
  328. sage/combinat/words/word.py +769 -0
  329. sage/combinat/words/word_char.cpython-314-x86_64-linux-musl.so +0 -0
  330. sage/combinat/words/word_char.pyx +847 -0
  331. sage/combinat/words/word_datatypes.cpython-314-x86_64-linux-musl.so +0 -0
  332. sage/combinat/words/word_datatypes.pxd +4 -0
  333. sage/combinat/words/word_datatypes.pyx +1067 -0
  334. sage/combinat/words/word_generators.py +2026 -0
  335. sage/combinat/words/word_infinite_datatypes.py +1218 -0
  336. sage/combinat/words/word_options.py +99 -0
  337. sage/combinat/words/words.py +2396 -0
  338. sage/data_structures/all__sagemath_combinat.py +1 -0
  339. sage/databases/all__sagemath_combinat.py +13 -0
  340. sage/databases/findstat.py +4897 -0
  341. sage/databases/oeis.py +2058 -0
  342. sage/databases/sloane.py +393 -0
  343. sage/dynamics/all__sagemath_combinat.py +14 -0
  344. sage/dynamics/cellular_automata/all.py +7 -0
  345. sage/dynamics/cellular_automata/catalog.py +34 -0
  346. sage/dynamics/cellular_automata/elementary.py +612 -0
  347. sage/dynamics/cellular_automata/glca.py +477 -0
  348. sage/dynamics/cellular_automata/solitons.py +1463 -0
  349. sage/dynamics/finite_dynamical_system.py +1249 -0
  350. sage/dynamics/finite_dynamical_system_catalog.py +382 -0
  351. sage/games/all.py +7 -0
  352. sage/games/hexad.py +704 -0
  353. sage/games/quantumino.py +591 -0
  354. sage/games/sudoku.py +889 -0
  355. sage/games/sudoku_backtrack.cpython-314-x86_64-linux-musl.so +0 -0
  356. sage/games/sudoku_backtrack.pyx +189 -0
  357. sage/groups/all__sagemath_combinat.py +1 -0
  358. sage/groups/indexed_free_group.py +489 -0
  359. sage/libs/all__sagemath_combinat.py +6 -0
  360. sage/libs/lrcalc/__init__.py +1 -0
  361. sage/libs/lrcalc/lrcalc.py +525 -0
  362. sage/libs/symmetrica/__init__.py +7 -0
  363. sage/libs/symmetrica/all.py +101 -0
  364. sage/libs/symmetrica/kostka.pxi +168 -0
  365. sage/libs/symmetrica/part.pxi +193 -0
  366. sage/libs/symmetrica/plet.pxi +42 -0
  367. sage/libs/symmetrica/sab.pxi +196 -0
  368. sage/libs/symmetrica/sb.pxi +332 -0
  369. sage/libs/symmetrica/sc.pxi +192 -0
  370. sage/libs/symmetrica/schur.pxi +956 -0
  371. sage/libs/symmetrica/symmetrica.cpython-314-x86_64-linux-musl.so +0 -0
  372. sage/libs/symmetrica/symmetrica.pxi +1172 -0
  373. sage/libs/symmetrica/symmetrica.pyx +39 -0
  374. sage/monoids/all.py +13 -0
  375. sage/monoids/automatic_semigroup.py +1054 -0
  376. sage/monoids/free_abelian_monoid.py +315 -0
  377. sage/monoids/free_abelian_monoid_element.cpython-314-x86_64-linux-musl.so +0 -0
  378. sage/monoids/free_abelian_monoid_element.pxd +16 -0
  379. sage/monoids/free_abelian_monoid_element.pyx +397 -0
  380. sage/monoids/free_monoid.py +335 -0
  381. sage/monoids/free_monoid_element.py +431 -0
  382. sage/monoids/hecke_monoid.py +65 -0
  383. sage/monoids/string_monoid.py +817 -0
  384. sage/monoids/string_monoid_element.py +547 -0
  385. sage/monoids/string_ops.py +143 -0
  386. sage/monoids/trace_monoid.py +972 -0
  387. sage/rings/all__sagemath_combinat.py +2 -0
  388. sage/sat/all.py +4 -0
  389. sage/sat/boolean_polynomials.py +405 -0
  390. sage/sat/converters/__init__.py +6 -0
  391. sage/sat/converters/anf2cnf.py +14 -0
  392. sage/sat/converters/polybori.py +611 -0
  393. sage/sat/solvers/__init__.py +5 -0
  394. sage/sat/solvers/cryptominisat.py +287 -0
  395. sage/sat/solvers/dimacs.py +783 -0
  396. sage/sat/solvers/picosat.py +228 -0
  397. sage/sat/solvers/sat_lp.py +156 -0
  398. sage/sat/solvers/satsolver.cpython-314-x86_64-linux-musl.so +0 -0
  399. sage/sat/solvers/satsolver.pxd +3 -0
  400. sage/sat/solvers/satsolver.pyx +405 -0
@@ -0,0 +1,3220 @@
1
+ # sage_setup: distribution = sagemath-combinat
2
+ # sage.doctest: needs sage.numerical.mip
3
+ r"""
4
+ A bijectionist's toolkit
5
+
6
+ AUTHORS:
7
+
8
+ - Alexander Grosz, Tobias Kietreiber, Stephan Pfannerer and Martin
9
+ Rubey (2020-2022): Initial version
10
+
11
+ Quick reference
12
+ ===============
13
+
14
+ .. csv-table::
15
+ :class: contentstable
16
+ :widths: 30, 70
17
+ :delim: |
18
+
19
+ :meth:`~Bijectionist.set_statistics` | Declare statistics that are preserved by the bijection.
20
+ :meth:`~Bijectionist.set_value_restrictions` | Restrict the values of the statistic on an element.
21
+ :meth:`~Bijectionist.set_constant_blocks` | Declare that the statistic is constant on some sets.
22
+ :meth:`~Bijectionist.set_distributions` | Restrict the distribution of values of the statistic on some elements.
23
+ :meth:`~Bijectionist.set_intertwining_relations` | Declare that the statistic intertwines with other maps.
24
+ :meth:`~Bijectionist.set_quadratic_relation` | Declare that the statistic satisfies a certain relation.
25
+ :meth:`~Bijectionist.set_homomesic` | Declare that the statistic is homomesic with respect to a given set partition.
26
+
27
+
28
+ :meth:`~Bijectionist.statistics_table` | Print a table collecting information on the given statistics.
29
+ :meth:`~Bijectionist.statistics_fibers` | Collect elements with the same statistics.
30
+
31
+ :meth:`~Bijectionist.constant_blocks` | Return the blocks on which the statistic is constant.
32
+ :meth:`~Bijectionist.solutions_iterator` | Iterate over all possible solutions.
33
+ :meth:`~Bijectionist.possible_values` | Return all possible values for a given element.
34
+ :meth:`~Bijectionist.minimal_subdistributions_iterator` | Iterate over the minimal subdistributions.
35
+ :meth:`~Bijectionist.minimal_subdistributions_blocks_iterator` | Iterate over the minimal subdistributions.
36
+
37
+ A guided tour
38
+ =============
39
+
40
+ Consider the following combinatorial statistics on a permutation:
41
+
42
+ * `wex`, the number of weak excedences,
43
+ * `fix`, the number of fixed points,
44
+ * `des`, the number of descents (after appending `0`),
45
+ * `adj`, the number of adjacencies (after appending `0`), and
46
+ * `llis`, the length of a longest increasing subsequence.
47
+
48
+ Moreover, let `rot` be action of rotation on a permutation, i.e., the
49
+ conjugation with the long cycle.
50
+
51
+ It is known that there must exist a statistic `s` on permutations,
52
+ which is equidistributed with `llis` but additionally invariant under
53
+ `rot`. However, at least very small cases do not contradict the
54
+ possibility that one can even find a statistic `s`, invariant under
55
+ `rot` and such that `(s, wex, fix) \sim (llis, des, adj)`. Let us
56
+ check this for permutations of size at most `3`::
57
+
58
+ sage: N = 3
59
+ sage: A = B = [pi for n in range(N+1) for pi in Permutations(n)]
60
+ sage: def alpha1(p): return len(p.weak_excedences())
61
+ sage: def alpha2(p): return len(p.fixed_points())
62
+ sage: def beta1(p): return len(p.descents(final_descent=True)) if p else 0
63
+ sage: def beta2(p): return len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1])
64
+ sage: tau = Permutation.longest_increasing_subsequence_length
65
+ sage: def rotate_permutation(p):
66
+ ....: cycle = Permutation(tuple(range(1, len(p)+1)))
67
+ ....: return Permutation([cycle.inverse()(p(cycle(i))) for i in range(1, len(p)+1)])
68
+ sage: bij = Bijectionist(A, B, tau)
69
+ sage: bij.set_statistics((len, len), (alpha1, beta1), (alpha2, beta2))
70
+ sage: a, b = bij.statistics_table()
71
+ sage: table(a, header_row=True, frame=True)
72
+ ┌───────────┬────────┬────────┬────────┐
73
+ │ a │ α_1(a) │ α_2(a) │ α_3(a) │
74
+ ╞═══════════╪════════╪════════╪════════╡
75
+ │ [] │ 0 │ 0 │ 0 │
76
+ ├───────────┼────────┼────────┼────────┤
77
+ │ [1] │ 1 │ 1 │ 1 │
78
+ ├───────────┼────────┼────────┼────────┤
79
+ │ [1, 2] │ 2 │ 2 │ 2 │
80
+ ├───────────┼────────┼────────┼────────┤
81
+ │ [2, 1] │ 2 │ 1 │ 0 │
82
+ ├───────────┼────────┼────────┼────────┤
83
+ │ [1, 2, 3] │ 3 │ 3 │ 3 │
84
+ ├───────────┼────────┼────────┼────────┤
85
+ │ [1, 3, 2] │ 3 │ 2 │ 1 │
86
+ ├───────────┼────────┼────────┼────────┤
87
+ │ [2, 1, 3] │ 3 │ 2 │ 1 │
88
+ ├───────────┼────────┼────────┼────────┤
89
+ │ [2, 3, 1] │ 3 │ 2 │ 0 │
90
+ ├───────────┼────────┼────────┼────────┤
91
+ │ [3, 1, 2] │ 3 │ 1 │ 0 │
92
+ ├───────────┼────────┼────────┼────────┤
93
+ │ [3, 2, 1] │ 3 │ 2 │ 1 │
94
+ └───────────┴────────┴────────┴────────┘
95
+
96
+ sage: table(b, header_row=True, frame=True)
97
+ ┌───────────┬───┬────────┬────────┬────────┐
98
+ │ b │ τ │ β_1(b) │ β_2(b) │ β_3(b) │
99
+ ╞═══════════╪═══╪════════╪════════╪════════╡
100
+ │ [] │ 0 │ 0 │ 0 │ 0 │
101
+ ├───────────┼───┼────────┼────────┼────────┤
102
+ │ [1] │ 1 │ 1 │ 1 │ 1 │
103
+ ├───────────┼───┼────────┼────────┼────────┤
104
+ │ [1, 2] │ 2 │ 2 │ 1 │ 0 │
105
+ ├───────────┼───┼────────┼────────┼────────┤
106
+ │ [2, 1] │ 1 │ 2 │ 2 │ 2 │
107
+ ├───────────┼───┼────────┼────────┼────────┤
108
+ │ [1, 2, 3] │ 3 │ 3 │ 1 │ 0 │
109
+ ├───────────┼───┼────────┼────────┼────────┤
110
+ │ [1, 3, 2] │ 2 │ 3 │ 2 │ 1 │
111
+ ├───────────┼───┼────────┼────────┼────────┤
112
+ │ [2, 1, 3] │ 2 │ 3 │ 2 │ 1 │
113
+ ├───────────┼───┼────────┼────────┼────────┤
114
+ │ [2, 3, 1] │ 2 │ 3 │ 2 │ 1 │
115
+ ├───────────┼───┼────────┼────────┼────────┤
116
+ │ [3, 1, 2] │ 2 │ 3 │ 2 │ 0 │
117
+ ├───────────┼───┼────────┼────────┼────────┤
118
+ │ [3, 2, 1] │ 1 │ 3 │ 3 │ 3 │
119
+ └───────────┴───┴────────┴────────┴────────┘
120
+
121
+ sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition
122
+ sage: bij.set_constant_blocks(orbit_decomposition(A, rotate_permutation))
123
+ sage: bij.constant_blocks()
124
+ {{[1, 3, 2], [2, 1, 3], [3, 2, 1]}}
125
+ sage: next(bij.solutions_iterator())
126
+ {[]: 0,
127
+ [1]: 1,
128
+ [1, 2]: 1,
129
+ [1, 2, 3]: 1,
130
+ [1, 3, 2]: 2,
131
+ [2, 1]: 2,
132
+ [2, 1, 3]: 2,
133
+ [2, 3, 1]: 2,
134
+ [3, 1, 2]: 3,
135
+ [3, 2, 1]: 2}
136
+
137
+ On the other hand, we can check that there is no rotation invariant
138
+ statistic on non-crossing set partitions which is equidistributed
139
+ with the Strahler number on ordered trees::
140
+
141
+ sage: N = 8
142
+ sage: A = [SetPartition(d.to_noncrossing_partition()) for n in range(N) for d in DyckWords(n)]
143
+ sage: B = [t for n in range(1, N+1) for t in OrderedTrees(n)]
144
+ sage: def theta(m): return SetPartition([[i % m.size() + 1 for i in b] for b in m])
145
+
146
+ Code for the Strahler number can be obtained from FindStat. The
147
+ following code is equivalent to ``tau = findstat(397)``::
148
+
149
+ sage: def tau(T):
150
+ ....: if len(T) == 0:
151
+ ....: return 1
152
+ ....: else:
153
+ ....: l = [tau(S) for S in T]
154
+ ....: m = max(l)
155
+ ....: if l.count(m) == 1:
156
+ ....: return m
157
+ ....: else:
158
+ ....: return m+1
159
+ sage: bij = Bijectionist(A, B, tau)
160
+ sage: bij.set_statistics((lambda a: a.size(), lambda b: b.node_number()-1))
161
+ sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition
162
+ sage: bij.set_constant_blocks(orbit_decomposition(A, theta))
163
+ sage: list(bij.solutions_iterator())
164
+ []
165
+
166
+ Next we demonstrate how to search for a bijection, instead An example
167
+ identifying `s` and `S`::
168
+
169
+ sage: N = 4
170
+ sage: A = [dyck_word for n in range(1, N) for dyck_word in DyckWords(n)]
171
+ sage: B = [binary_tree for n in range(1, N) for binary_tree in BinaryTrees(n)]
172
+ sage: concat_path = lambda D1, D2: DyckWord(list(D1) + list(D2))
173
+ sage: concat_tree = lambda B1, B2: concat_path(B1.to_dyck_word(),
174
+ ....: B2.to_dyck_word()).to_binary_tree()
175
+ sage: bij = Bijectionist(A, B)
176
+ sage: bij.set_intertwining_relations((2, concat_path, concat_tree))
177
+ sage: bij.set_statistics((lambda d: d.semilength(), lambda t: t.node_number()))
178
+ sage: for D in sorted(bij.minimal_subdistributions_iterator(), key=lambda x: (len(x[0][0]), x)):
179
+ ....: ascii_art(D)
180
+ ( [ /\ ], [ o ] )
181
+ ( [ o ] )
182
+ ( [ \ ] )
183
+ ( [ /\/\ ], [ o ] )
184
+ ( [ o ] )
185
+ ( [ /\ ] [ / ] )
186
+ ( [ / \ ], [ o ] )
187
+ ( [ o ] )
188
+ ( [ \ ] )
189
+ ( [ o ] )
190
+ ( [ \ ] )
191
+ ( [ /\/\/\ ], [ o ] )
192
+ ( [ o ] )
193
+ ( [ \ ] )
194
+ ( [ o ] )
195
+ ( [ /\ ] [ / ] )
196
+ ( [ /\/ \ ], [ o ] )
197
+ ( [ o ] )
198
+ ( [ /\ ] [ / \ ] )
199
+ ( [ / \/\ ], [ o o ] )
200
+ ( [ o, o ] )
201
+ ( [ / / ] )
202
+ ( [ /\ ] [ o o ] )
203
+ ( [ /\/\ / \ ] [ \ / ] )
204
+ ( [ / \, / \ ], [ o o ] )
205
+
206
+ The output is in a form suitable for FindStat::
207
+
208
+ sage: findmap(list(bij.minimal_subdistributions_iterator())) # optional -- internet
209
+ 0: Mp00034 (quality [100])
210
+ 1: Mp00061oMp00023 (quality [100])
211
+ 2: Mp00018oMp00140 (quality [100])
212
+
213
+ TESTS::
214
+
215
+ sage: N = 4; A = B = [permutation for n in range(N) for permutation in Permutations(n)]
216
+ sage: def theta(pi): return Permutation([x+1 if x != len(pi) else 1 for x in pi[-1:]+pi[:-1]])
217
+ sage: def tau(pi):
218
+ ....: n = len(pi)
219
+ ....: return sum([1 for i in range(1, n+1) for j in range(1, n+1)
220
+ ....: if i<j <= pi(i)<pi(j) or pi(i)<pi(j)<i<j])
221
+ sage: bij = Bijectionist(A, B, tau)
222
+ sage: bij.set_statistics((len, len))
223
+ sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition
224
+ sage: bij.set_constant_blocks(orbit_decomposition(A, theta))
225
+ sage: for solution in sorted(bij.solutions_iterator(), key=lambda d: sorted(d.items())):
226
+ ....: print(solution)
227
+ {[]: 0, [1]: 0, [1, 2]: 0, [2, 1]: 0, [1, 2, 3]: 0, [1, 3, 2]: 0, [2, 1, 3]: 0, [3, 2, 1]: 0, [2, 3, 1]: 0, [3, 1, 2]: 1}
228
+ {[]: 0, [1]: 0, [1, 2]: 0, [2, 1]: 0, [1, 2, 3]: 0, [1, 3, 2]: 0, [2, 1, 3]: 0, [3, 2, 1]: 0, [2, 3, 1]: 1, [3, 1, 2]: 0}
229
+ {[]: 0, [1]: 0, [1, 2]: 0, [2, 1]: 0, [1, 2, 3]: 1, [1, 3, 2]: 0, [2, 1, 3]: 0, [3, 2, 1]: 0, [2, 3, 1]: 0, [3, 1, 2]: 0}
230
+
231
+ A test including intertwining relations::
232
+
233
+ sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)]
234
+ sage: def alpha(D): return (D.area(), D.bounce())
235
+ sage: def beta(D): return (D.bounce(), D.area())
236
+ sage: def tau(D): return D.number_of_touch_points()
237
+
238
+ The following looks correct::
239
+
240
+ sage: bij = Bijectionist(A, B, tau)
241
+ sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength()))
242
+ sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
243
+ ....: print(solution)
244
+ {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 1, [1, 1, 0, 0]: 2}
245
+ {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1}
246
+
247
+ The following looks correct, because `alpha = beta \circ S` forces
248
+ `S([1,0,1,0]) = [1,1,0,0]` and `s = tau \circ S` forces therefore `s([1,0,1,0])
249
+ = \tau(S([1,0,1,0])) = \tau([1,1,0,0]) = 1`::
250
+
251
+ sage: bij = Bijectionist(A, B, tau)
252
+ sage: bij.set_statistics((alpha, beta), (lambda d: d.semilength(), lambda d: d.semilength()))
253
+ sage: for solution in bij.solutions_iterator():
254
+ ....: print(solution)
255
+ {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 1, [1, 1, 0, 0]: 2}
256
+
257
+ Now we introduce a intertwining relation::
258
+
259
+ sage: concat_path = lambda D1, D2: DyckWord(list(D1) + list(D2))
260
+ sage: pi_rho = (2, concat_path, lambda x, y: x+y)
261
+
262
+ Without `\alpha` and `\beta` but with `\pi` and `\rho` the other values are
263
+ forced because `s([1,0,1,0]) = s(\pi([1,0], [1,0])) = \rho(s([1,0]), s([1,0]))
264
+ = 2`::
265
+
266
+ sage: bij = Bijectionist(A, B, tau)
267
+ sage: bij.set_intertwining_relations(pi_rho)
268
+ sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength()))
269
+ sage: for solution in bij.solutions_iterator():
270
+ ....: print(solution)
271
+ {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1}
272
+
273
+ Thus the combination of both constraints should be infeasible::
274
+
275
+ sage: bij = Bijectionist(A, B, tau)
276
+ sage: bij.set_statistics((alpha, beta), (lambda d: d.semilength(), lambda d: d.semilength()))
277
+ sage: bij.set_intertwining_relations(pi_rho)
278
+ sage: list(bij.solutions_iterator())
279
+ []
280
+
281
+ Repeating some tests, but using the constructor instead of set_XXX() methods:
282
+
283
+ sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)]
284
+ sage: def alpha(D): return (D.area(), D.bounce())
285
+ sage: def beta(D): return (D.bounce(), D.area())
286
+ sage: def tau(D): return D.number_of_touch_points()
287
+
288
+ sage: bij = Bijectionist(A, B, tau, alpha_beta=((lambda d: d.semilength(), lambda d: d.semilength()),))
289
+ sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
290
+ ....: print(solution)
291
+ {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 1, [1, 1, 0, 0]: 2}
292
+ {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1}
293
+
294
+ Constant blocks::
295
+
296
+ sage: A = B = 'abcd'
297
+ sage: def pi(p1, p2): return 'abcdefgh'[A.index(p1) + A.index(p2)]
298
+ sage: def rho(s1, s2): return (s1 + s2) % 2
299
+ sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2, P=[['a', 'c']], pi_rho=((2, pi, rho),))
300
+ sage: list(bij.solutions_iterator())
301
+ [{'a': 0, 'b': 1, 'c': 0, 'd': 1}]
302
+ sage: bij.constant_blocks()
303
+ {{'a', 'c'}, {'b', 'd'}}
304
+
305
+ Distributions::
306
+
307
+ sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
308
+ sage: tau = Permutation.longest_increasing_subsequence_length
309
+ sage: bij = Bijectionist(A, B, tau, alpha_beta=((len, len),), elements_distributions=(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [1, 3]),))
310
+ sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
311
+ ....: print(sol)
312
+ {[]: 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}
313
+ {[]: 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}
314
+ {[]: 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}
315
+ {[]: 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}
316
+
317
+ Intertwining relations::
318
+
319
+ sage: def concat(p1, p2): return Permutation(p1 + [i + len(p1) for i in p2])
320
+
321
+ sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
322
+ sage: bij = Bijectionist(A, B, Permutation.number_of_fixed_points, alpha_beta=((len, len),), pi_rho=((2, concat, lambda x, y: x + y),))
323
+ sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
324
+ ....: print(solution)
325
+ {[]: 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}
326
+ {[]: 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}
327
+ {[]: 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}
328
+
329
+ Statistics::
330
+
331
+ sage: N = 4; A = B = [permutation for n in range(N) for permutation in Permutations(n)]
332
+ sage: def wex(p): return len(p.weak_excedences())
333
+ sage: def fix(p): return len(p.fixed_points())
334
+ sage: def des(p): return len(p.descents(final_descent=True)) if p else 0
335
+ sage: bij = Bijectionist(A, B, fix, alpha_beta=((wex, des), (len, len)))
336
+ sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
337
+ ....: print(solution)
338
+ {[]: 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}
339
+ {[]: 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}
340
+ {[]: 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}
341
+ {[]: 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}
342
+ {[]: 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}
343
+ {[]: 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}
344
+
345
+ Value restrictions::
346
+
347
+ sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
348
+ sage: tau = Permutation.longest_increasing_subsequence_length
349
+ sage: alpha_beta = [(len, len)]
350
+ sage: value_restrictions = [(Permutation([1, 2]), [1]), (Permutation([3, 2, 1]), [2, 3, 4])]
351
+ sage: bij = Bijectionist(A, B, tau, alpha_beta=alpha_beta, value_restrictions=value_restrictions)
352
+ sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
353
+ ....: print(sol)
354
+ {[]: 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}
355
+ ...
356
+ {[]: 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}
357
+
358
+ sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
359
+ sage: tau = Permutation.longest_increasing_subsequence_length
360
+ sage: bij = Bijectionist(A, B, tau, value_restrictions=((Permutation([1, 2]), [4, 5]),))
361
+ sage: next(bij.solutions_iterator())
362
+ Traceback (most recent call last):
363
+ ...
364
+ ValueError: no possible values found for singleton block [[1, 2]]
365
+ """
366
+ # ****************************************************************************
367
+ # Copyright (C) 2020 Martin Rubey <martin.rubey at tuwien.ac.at>
368
+ # Stephan Pfannerer
369
+ # Tobias Kietreiber
370
+ # Alexander Grosz
371
+ #
372
+ # Distributed under the terms of the GNU General Public License (GPL)
373
+ #
374
+ # This code is distributed in the hope that it will be useful,
375
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
376
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
377
+ # General Public License for more details.
378
+ #
379
+ # The full text of the GPL is available at:
380
+ #
381
+ # https://www.gnu.org/licenses/
382
+ # ***************************************************************************
383
+ import itertools
384
+ from collections import namedtuple, defaultdict
385
+ from sage.numerical.mip import MixedIntegerLinearProgram, MIPSolverException
386
+ from sage.rings.integer_ring import ZZ
387
+ from sage.combinat.set_partition import SetPartition, SetPartitions
388
+ from sage.sets.disjoint_set import DisjointSet
389
+ from sage.structure.sage_object import SageObject
390
+ from copy import copy
391
+ from sage.misc.verbose import get_verbose
392
+
393
+
394
+ class Bijectionist(SageObject):
395
+ r"""
396
+ A toolbox to list all possible bijections between two finite sets
397
+ under various constraints.
398
+
399
+ INPUT:
400
+
401
+ - ``A``, ``B`` -- sets of equal size, given as a list
402
+
403
+ - ``tau`` -- (optional) a function from ``B`` to ``Z``, in case of
404
+ ``None``, the identity map ``lambda x: x`` is used
405
+
406
+ - ``alpha_beta`` -- (optional) a list of pairs of statistics ``alpha`` from
407
+ ``A`` to ``W`` and ``beta`` from ``B`` to ``W``
408
+
409
+ - ``P`` -- (optional) a partition of ``A``
410
+
411
+ - ``pi_rho`` -- (optional) a list of triples ``(k, pi, rho)``, where
412
+
413
+ * ``pi`` -- a ``k``-ary operation composing objects in ``A`` and
414
+ * ``rho`` -- a ``k``-ary function composing statistic values in ``Z``
415
+
416
+ - ``elements_distributions`` -- (optional) a list of pairs ``(tA, tZ)``,
417
+ specifying the distributions of ``tA``
418
+
419
+ - ``value_restrictions`` -- (optional) a list of pairs ``(a, tZ)``,
420
+ restricting the possible values of ``a``
421
+
422
+ - ``solver`` -- (optional) the backend used to solve the mixed integer
423
+ linear programs
424
+
425
+ ``W`` and ``Z`` can be arbitrary sets. As a natural example we may think
426
+ of the natural numbers or tuples of integers.
427
+
428
+ We are looking for a statistic `s: A\to Z` and a bijection `S: A\to B` such
429
+ that
430
+
431
+ - `s = \tau \circ S`: the statistics `s` and `\tau` are equidistributed and
432
+ `S` is an intertwining bijection.
433
+
434
+ - `\alpha = \beta \circ S`: the statistics `\alpha` and `\beta` are
435
+ equidistributed and `S` is an intertwining bijection.
436
+
437
+ - `s` is constant on the blocks of `P`.
438
+
439
+ - `s(\pi(a_1,\dots, a_k)) = \rho(s(a_1),\dots, s(a_k))`.
440
+
441
+ Additionally, we may require that
442
+
443
+ - `s(a)\in Z_a` for specified sets `Z_a\subseteq Z`, and
444
+
445
+ - `s|_{\tilde A}` has a specified distribution for specified sets `\tilde A
446
+ \subset A`.
447
+
448
+ If `\tau` is the identity, the two unknown functions `s` and `S` coincide.
449
+ Although we do not exclude other bijective choices for `\tau`, they
450
+ probably do not make sense.
451
+
452
+ If we want that `S` is graded, i.e. if elements of `A` and `B` have a
453
+ notion of size and `S` should preserve this size, we can add grading
454
+ statistics as `\alpha` and `\beta`. Since `\alpha` and `\beta` will be
455
+ equidistributed with `S` as an intertwining bijection, `S` will then also
456
+ be graded.
457
+
458
+ In summary, we have the following two commutative diagrams, where `s` and
459
+ `S` are unknown functions.
460
+
461
+ .. MATH::
462
+
463
+ \begin{array}{rrl}
464
+ & A \\
465
+ {\scriptstyle\alpha}\swarrow & {\scriptstyle S}\downarrow & \searrow{\scriptstyle s}\\
466
+ W \overset{\beta}{\leftarrow} & B & \overset{\tau}{\rightarrow} Z
467
+ \end{array}
468
+ \qquad
469
+ \begin{array}{lcl}
470
+ A^k &\overset{\pi}{\rightarrow} & A\\
471
+ \downarrow{\scriptstyle s^k} & & \downarrow{\scriptstyle s}\\
472
+ Z^k &\overset{\rho}{\rightarrow} & Z\\
473
+ \end{array}
474
+
475
+ .. NOTE::
476
+
477
+ If `\tau` is the identity map, the partition `P` of `A` necessarily
478
+ consists only of singletons.
479
+
480
+ .. NOTE::
481
+
482
+ The order of invocation of the methods with prefix ``set``, i.e.,
483
+ :meth:`set_statistics`, :meth:`set_intertwining_relations`,
484
+ :meth:`set_constant_blocks`, etc., is irrelevant. Calling any of these
485
+ methods a second time overrides the previous specification.
486
+ """
487
+ def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=None,
488
+ pi_rho=tuple(), phi_psi=tuple(), Q=None,
489
+ elements_distributions=tuple(),
490
+ value_restrictions=tuple(), solver=None, key=None):
491
+ """
492
+ Initialize the bijectionist.
493
+
494
+ TESTS:
495
+
496
+ Check that large input sets are handled well::
497
+
498
+ sage: A = B = range(20000)
499
+ sage: bij = Bijectionist(A, B)
500
+ """
501
+ # glossary of standard letters:
502
+ # A, B, Z, W ... finite sets
503
+ # P ... set partition of A
504
+ # tA, tB, tZ, tP ... subsets
505
+ # a in A, b in B, p in P
506
+ # S: A -> B
507
+ # alpha: A -> W, beta: B -> W
508
+ # s: A -> Z, tau: B -> Z
509
+ # k arity of pi and rho
510
+ # pi: A^k -> A, rho: Z^k -> Z
511
+ # a_tuple in A^k
512
+ self._A = list(A)
513
+ self._B = list(B)
514
+ assert len(self._A) == len(set(self._A)), "A must have distinct items"
515
+ assert len(self._B) == len(set(self._B)), "B must have distinct items"
516
+ self._bmilp = None
517
+ self._sorter = {}
518
+ self._sorter["A"] = lambda x: sorted(x, key=self._A.index)
519
+ self._sorter["B"] = lambda x: sorted(x, key=self._B.index)
520
+
521
+ if tau is None:
522
+ self._tau = {b: b for b in self._B}
523
+ else:
524
+ self._tau = {b: tau(b) for b in self._B}
525
+ # we store Z as a list to keep an order
526
+ self._Z = set(self._tau.values())
527
+ if key is not None and "Z" in key:
528
+ self._sorter["Z"] = lambda x: sorted(x, key=key["Z"])
529
+ self._Z = self._sorter["Z"](self._Z)
530
+ else:
531
+ try:
532
+ self._Z = sorted(self._Z)
533
+ self._sorter["Z"] = lambda x: sorted(x)
534
+ except TypeError:
535
+ self._Z = list(self._Z)
536
+ self._sorter["Z"] = lambda x: list(x)
537
+ if P is None:
538
+ P = []
539
+
540
+ # set optional inputs
541
+ self.set_statistics(*alpha_beta)
542
+ self.set_value_restrictions(*value_restrictions)
543
+ self.set_distributions(*elements_distributions)
544
+ self.set_quadratic_relation(*phi_psi)
545
+ self.set_homomesic(Q)
546
+ self.set_intertwining_relations(*pi_rho)
547
+ self.set_constant_blocks(P)
548
+
549
+ self._solver = solver
550
+
551
+ def set_constant_blocks(self, P):
552
+ r"""
553
+ Declare that `s: A\to Z` is constant on each block of `P`.
554
+
555
+ .. WARNING::
556
+
557
+ Any restriction imposed by a previous invocation of
558
+ :meth:`set_constant_blocks` will be overwritten,
559
+ including restrictions discovered by
560
+ :meth:`set_intertwining_relations` and
561
+ :meth:`solutions_iterator`!
562
+
563
+ A common example is to use the orbits of a bijection acting
564
+ on `A`. This can be achieved using the function
565
+ :meth:`~sage.combinat.cyclic_sieving_phenomenon.orbit_decomposition`.
566
+
567
+ INPUT:
568
+
569
+ - ``P`` -- set partition of `A`, singletons may be omitted
570
+
571
+ EXAMPLES:
572
+
573
+ Initially the partitions are set to singleton blocks. The
574
+ current partition can be reviewed using
575
+ :meth:`constant_blocks`::
576
+
577
+ sage: A = B = 'abcd'
578
+ sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2)
579
+ sage: bij.constant_blocks()
580
+ {}
581
+
582
+ sage: bij.set_constant_blocks([['a', 'c']])
583
+ sage: bij.constant_blocks()
584
+ {{'a', 'c'}}
585
+
586
+ We now add a map that combines some blocks::
587
+
588
+ sage: def pi(p1, p2): return 'abcdefgh'[A.index(p1) + A.index(p2)]
589
+ sage: def rho(s1, s2): return (s1 + s2) % 2
590
+ sage: bij.set_intertwining_relations((2, pi, rho))
591
+ sage: list(bij.solutions_iterator())
592
+ [{'a': 0, 'b': 1, 'c': 0, 'd': 1}]
593
+ sage: bij.constant_blocks()
594
+ {{'a', 'c'}, {'b', 'd'}}
595
+
596
+ Setting constant blocks overrides any previous assignment::
597
+
598
+ sage: bij.set_constant_blocks([['a', 'b']])
599
+ sage: bij.constant_blocks()
600
+ {{'a', 'b'}}
601
+
602
+ If there is no solution, and the coarsest partition is
603
+ requested, an error is raised::
604
+
605
+ sage: bij.constant_blocks(optimal=True)
606
+ Traceback (most recent call last):
607
+ ...
608
+ StopIteration
609
+ """
610
+ self._bmilp = None
611
+ self._P = DisjointSet(self._A)
612
+ P = sorted(self._sorter["A"](p) for p in P)
613
+ for p in P:
614
+ for a in p:
615
+ self._P.union(p[0], a)
616
+
617
+ self._possible_block_values = None
618
+
619
+ def constant_blocks(self, singletons=False, optimal=False):
620
+ r"""
621
+ Return the set partition `P` of `A` such that `s: A\to Z` is
622
+ known to be constant on the blocks of `P`.
623
+
624
+ INPUT:
625
+
626
+ - ``singletons`` -- boolean (default: ``False``); whether or not to
627
+ include singleton blocks in the output
628
+
629
+ - ``optimal`` -- boolean (default: ``False``); whether or not to
630
+ compute the coarsest possible partition
631
+
632
+ .. NOTE::
633
+
634
+ computing the coarsest possible partition may be
635
+ computationally expensive, but may speed up generating
636
+ solutions.
637
+
638
+ EXAMPLES::
639
+
640
+ sage: A = B = ["a", "b", "c"]
641
+ sage: bij = Bijectionist(A, B, lambda x: 0)
642
+ sage: bij.set_constant_blocks([["a", "b"]])
643
+ sage: bij.constant_blocks()
644
+ {{'a', 'b'}}
645
+
646
+ sage: bij.constant_blocks(singletons=True)
647
+ {{'a', 'b'}, {'c'}}
648
+ """
649
+ if optimal:
650
+ self._forced_constant_blocks()
651
+ if singletons:
652
+ return SetPartition(self._P)
653
+ return SetPartition(p for p in self._P if len(p) > 1)
654
+
655
+ def set_statistics(self, *alpha_beta):
656
+ r"""
657
+ Set constraints of the form `\alpha = \beta\circ S`.
658
+
659
+ .. WARNING::
660
+
661
+ Any restriction imposed by a previous invocation of
662
+ :meth:`set_statistics` will be overwritten!
663
+
664
+ INPUT:
665
+
666
+ - ``alpha_beta`` -- one or more pairs `(\alpha: A\to W,
667
+ \beta: B\to W)`
668
+
669
+ If the statistics `\alpha` and `\beta` are not
670
+ equidistributed, an error is raised.
671
+
672
+ ALGORITHM:
673
+
674
+ We add
675
+
676
+ .. MATH::
677
+
678
+ \sum_{a\in A, z\in Z} x_{p(a), z} s^z t^{\alpha(a)}
679
+ = \sum_{b\in B} s^{\tau(b)} t(\beta(b))
680
+
681
+ as a matrix equation to the MILP.
682
+
683
+ EXAMPLES:
684
+
685
+ We look for bijections `S` on permutations such that the
686
+ number of weak exceedences of `S(\pi)` equals the number of
687
+ descents of `\pi`, and statistics `s`, such that the number
688
+ of fixed points of `S(\pi)` equals `s(\pi)`::
689
+
690
+ sage: N = 4; A = B = [permutation for n in range(N) for permutation in Permutations(n)]
691
+ sage: def wex(p): return len(p.weak_excedences())
692
+ sage: def fix(p): return len(p.fixed_points())
693
+ sage: def des(p): return len(p.descents(final_descent=True)) if p else 0
694
+ sage: def adj(p): return len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1])
695
+ sage: bij = Bijectionist(A, B, fix)
696
+ sage: bij.set_statistics((wex, des), (len, len))
697
+ sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
698
+ ....: print(solution)
699
+ {[]: 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}
700
+ {[]: 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}
701
+ {[]: 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}
702
+ {[]: 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}
703
+ {[]: 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}
704
+ {[]: 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}
705
+
706
+ sage: bij = Bijectionist(A, B, fix)
707
+ sage: bij.set_statistics((wex, des), (fix, adj), (len, len))
708
+ sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
709
+ ....: print(solution)
710
+ {[]: 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}
711
+ {[]: 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}
712
+ {[]: 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}
713
+
714
+ Calling this with non-equidistributed statistics yields an error::
715
+
716
+ sage: bij = Bijectionist(A, B, fix)
717
+ sage: bij.set_statistics((wex, fix))
718
+ Traceback (most recent call last):
719
+ ...
720
+ ValueError: statistics alpha and beta are not equidistributed
721
+
722
+ TESTS:
723
+
724
+ Calling ``set_statistics`` without arguments should restore the previous state::
725
+
726
+ sage: N = 3; A = B = [permutation for n in range(N) for permutation in Permutations(n)]
727
+ sage: def wex(p): return len(p.weak_excedences())
728
+ sage: def fix(p): return len(p.fixed_points())
729
+ sage: def des(p): return len(p.descents(final_descent=True)) if p else 0
730
+ sage: bij = Bijectionist(A, B, fix)
731
+ sage: bij.set_statistics((wex, des), (len, len))
732
+ sage: for solution in bij.solutions_iterator():
733
+ ....: print(solution)
734
+ {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2}
735
+ sage: bij.set_statistics()
736
+ sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
737
+ ....: print(solution)
738
+ {[]: 0, [1]: 0, [1, 2]: 1, [2, 1]: 2}
739
+ {[]: 0, [1]: 0, [1, 2]: 2, [2, 1]: 1}
740
+ {[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2}
741
+ {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0}
742
+ {[]: 0, [1]: 2, [1, 2]: 0, [2, 1]: 1}
743
+ {[]: 0, [1]: 2, [1, 2]: 1, [2, 1]: 0}
744
+ {[]: 1, [1]: 0, [1, 2]: 0, [2, 1]: 2}
745
+ {[]: 1, [1]: 0, [1, 2]: 2, [2, 1]: 0}
746
+ {[]: 1, [1]: 2, [1, 2]: 0, [2, 1]: 0}
747
+ {[]: 2, [1]: 0, [1, 2]: 0, [2, 1]: 1}
748
+ {[]: 2, [1]: 0, [1, 2]: 1, [2, 1]: 0}
749
+ {[]: 2, [1]: 1, [1, 2]: 0, [2, 1]: 0}
750
+ """
751
+ self._bmilp = None
752
+ self._n_statistics = len(alpha_beta)
753
+ # TODO: do we really want to recompute statistics every time?
754
+ self._alpha = lambda p: tuple(arg[0](p) for arg in alpha_beta)
755
+ self._beta = lambda p: tuple(arg[1](p) for arg in alpha_beta)
756
+
757
+ # generate fibers
758
+ self._statistics_fibers = {}
759
+ for a in self._A:
760
+ v = self._alpha(a)
761
+ if v not in self._statistics_fibers:
762
+ self._statistics_fibers[v] = ([], [])
763
+ self._statistics_fibers[v][0].append(a)
764
+
765
+ for b in self._B:
766
+ v = self._beta(b)
767
+ if v not in self._statistics_fibers:
768
+ raise ValueError(f"statistics alpha and beta do not have the same image, {v} is not a value of alpha, but of beta")
769
+ self._statistics_fibers[v][1].append(b)
770
+
771
+ # check compatibility
772
+ if not all(len(fiber[0]) == len(fiber[1])
773
+ for fiber in self._statistics_fibers.values()):
774
+ raise ValueError("statistics alpha and beta are not equidistributed")
775
+
776
+ self._W = list(self._statistics_fibers)
777
+
778
+ # the possible values of s(a) are tau(beta^{-1}(alpha(a)))
779
+ tau_beta_inverse = {}
780
+ self._statistics_possible_values = {}
781
+ for a in self._A:
782
+ v = self._alpha(a)
783
+ if v not in tau_beta_inverse:
784
+ tau_beta_inverse[v] = set(self._tau[b]
785
+ for b in self._statistics_fibers[v][1])
786
+ self._statistics_possible_values[a] = tau_beta_inverse[v]
787
+
788
+ def statistics_fibers(self):
789
+ r"""
790
+ Return a dictionary mapping statistic values in `W` to their
791
+ preimages in `A` and `B`.
792
+
793
+ This is a (computationally) fast way to obtain a first
794
+ impression which objects in `A` should be mapped to which
795
+ objects in `B`.
796
+
797
+ EXAMPLES::
798
+
799
+ sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
800
+ sage: tau = Permutation.longest_increasing_subsequence_length
801
+ sage: def wex(p): return len(p.weak_excedences())
802
+ sage: def fix(p): return len(p.fixed_points())
803
+ sage: def des(p): return len(p.descents(final_descent=True)) if p else 0
804
+ sage: def adj(p): return len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1])
805
+ sage: bij = Bijectionist(A, B, tau)
806
+ sage: bij.set_statistics((len, len), (wex, des), (fix, adj))
807
+ sage: table([[key, AB[0], AB[1]] for key, AB in bij.statistics_fibers().items()])
808
+ (0, 0, 0) [[]] [[]]
809
+ (1, 1, 1) [[1]] [[1]]
810
+ (2, 2, 2) [[1, 2]] [[2, 1]]
811
+ (2, 1, 0) [[2, 1]] [[1, 2]]
812
+ (3, 3, 3) [[1, 2, 3]] [[3, 2, 1]]
813
+ (3, 2, 1) [[1, 3, 2], [2, 1, 3], [3, 2, 1]] [[1, 3, 2], [2, 1, 3], [2, 3, 1]]
814
+ (3, 2, 0) [[2, 3, 1]] [[3, 1, 2]]
815
+ (3, 1, 0) [[3, 1, 2]] [[1, 2, 3]]
816
+ """
817
+ return self._statistics_fibers
818
+
819
+ def statistics_table(self, header=True):
820
+ r"""
821
+ Provide information about all elements of `A` with corresponding
822
+ `\alpha` values and all elements of `B` with corresponding
823
+ `\beta` and `\tau` values.
824
+
825
+ INPUT:
826
+
827
+ - ``header`` -- boolean (default: ``True``); whether to include a
828
+ header with the standard Greek letters
829
+
830
+ OUTPUT:
831
+
832
+ A pair of lists suitable for :class:`~sage.misc.table.table`,
833
+ where
834
+
835
+ - the first contains the elements of `A` together with the
836
+ values of `\alpha`
837
+
838
+ - the second contains the elements of `B` together with the
839
+ values of `\tau` and `\beta`
840
+
841
+ EXAMPLES::
842
+
843
+ sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
844
+ sage: tau = Permutation.longest_increasing_subsequence_length
845
+ sage: def wex(p): return len(p.weak_excedences())
846
+ sage: def fix(p): return len(p.fixed_points())
847
+ sage: def des(p): return len(p.descents(final_descent=True)) if p else 0
848
+ sage: def adj(p): return len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1])
849
+ sage: bij = Bijectionist(A, B, tau)
850
+ sage: bij.set_statistics((wex, des), (fix, adj))
851
+ sage: a, b = bij.statistics_table()
852
+ sage: table(a, header_row=True, frame=True)
853
+ ┌───────────┬────────┬────────┐
854
+ │ a │ α_1(a) │ α_2(a) │
855
+ ╞═══════════╪════════╪════════╡
856
+ │ [] │ 0 │ 0 │
857
+ ├───────────┼────────┼────────┤
858
+ │ [1] │ 1 │ 1 │
859
+ ├───────────┼────────┼────────┤
860
+ │ [1, 2] │ 2 │ 2 │
861
+ ├───────────┼────────┼────────┤
862
+ │ [2, 1] │ 1 │ 0 │
863
+ ├───────────┼────────┼────────┤
864
+ │ [1, 2, 3] │ 3 │ 3 │
865
+ ├───────────┼────────┼────────┤
866
+ │ [1, 3, 2] │ 2 │ 1 │
867
+ ├───────────┼────────┼────────┤
868
+ │ [2, 1, 3] │ 2 │ 1 │
869
+ ├───────────┼────────┼────────┤
870
+ │ [2, 3, 1] │ 2 │ 0 │
871
+ ├───────────┼────────┼────────┤
872
+ │ [3, 1, 2] │ 1 │ 0 │
873
+ ├───────────┼────────┼────────┤
874
+ │ [3, 2, 1] │ 2 │ 1 │
875
+ └───────────┴────────┴────────┘
876
+ sage: table(b, header_row=True, frame=True)
877
+ ┌───────────┬───┬────────┬────────┐
878
+ │ b │ τ │ β_1(b) │ β_2(b) │
879
+ ╞═══════════╪═══╪════════╪════════╡
880
+ │ [] │ 0 │ 0 │ 0 │
881
+ ├───────────┼───┼────────┼────────┤
882
+ │ [1] │ 1 │ 1 │ 1 │
883
+ ├───────────┼───┼────────┼────────┤
884
+ │ [1, 2] │ 2 │ 1 │ 0 │
885
+ ├───────────┼───┼────────┼────────┤
886
+ │ [2, 1] │ 1 │ 2 │ 2 │
887
+ ├───────────┼───┼────────┼────────┤
888
+ │ [1, 2, 3] │ 3 │ 1 │ 0 │
889
+ ├───────────┼───┼────────┼────────┤
890
+ │ [1, 3, 2] │ 2 │ 2 │ 1 │
891
+ ├───────────┼───┼────────┼────────┤
892
+ │ [2, 1, 3] │ 2 │ 2 │ 1 │
893
+ ├───────────┼───┼────────┼────────┤
894
+ │ [2, 3, 1] │ 2 │ 2 │ 1 │
895
+ ├───────────┼───┼────────┼────────┤
896
+ │ [3, 1, 2] │ 2 │ 2 │ 0 │
897
+ ├───────────┼───┼────────┼────────┤
898
+ │ [3, 2, 1] │ 1 │ 3 │ 3 │
899
+ └───────────┴───┴────────┴────────┘
900
+
901
+ TESTS:
902
+
903
+ If no statistics are given, the table should still be able to be generated::
904
+
905
+ sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)]
906
+ sage: tau = Permutation.longest_increasing_subsequence_length
907
+ sage: bij = Bijectionist(A, B, tau)
908
+ sage: a, b = bij.statistics_table()
909
+ sage: table(a, header_row=True, frame=True)
910
+ ┌────────┐
911
+ │ a │
912
+ ╞════════╡
913
+ │ [] │
914
+ ├────────┤
915
+ │ [1] │
916
+ ├────────┤
917
+ │ [1, 2] │
918
+ ├────────┤
919
+ │ [2, 1] │
920
+ └────────┘
921
+ sage: table(b, header_row=True, frame=True)
922
+ ┌────────┬───┐
923
+ │ b │ τ │
924
+ ╞════════╪═══╡
925
+ │ [] │ 0 │
926
+ ├────────┼───┤
927
+ │ [1] │ 1 │
928
+ ├────────┼───┤
929
+ │ [1, 2] │ 2 │
930
+ ├────────┼───┤
931
+ │ [2, 1] │ 1 │
932
+ └────────┴───┘
933
+
934
+ We can omit the header::
935
+
936
+ sage: bij.statistics_table(header=True)[1]
937
+ [['b', 'τ'], [[], 0], [[1], 1], [[1, 2], 2], [[2, 1], 1]]
938
+ sage: bij.statistics_table(header=False)[1]
939
+ [[[], 0], [[1], 1], [[1, 2], 2], [[2, 1], 1]]
940
+ """
941
+ # table for alpha
942
+ n_statistics = self._n_statistics
943
+ if header:
944
+ output_alphas = [["a"] + ["\u03b1_" + str(i) + "(a)"
945
+ for i in range(1, n_statistics + 1)]]
946
+ else:
947
+ output_alphas = []
948
+
949
+ for a in self._A:
950
+ if n_statistics > 0:
951
+ output_alphas.append([a] + list(self._alpha(a)))
952
+ else:
953
+ output_alphas.append([a])
954
+
955
+ # table for beta and tau
956
+ if header:
957
+ output_tau_betas = [["b", "\u03c4"] + ["\u03b2_" + str(i) + "(b)"
958
+ for i in range(1, n_statistics + 1)]]
959
+ else:
960
+ output_tau_betas = []
961
+ for b in self._B:
962
+ if n_statistics > 0:
963
+ output_tau_betas.append([b, self._tau[b]] + list(self._beta(b)))
964
+ else:
965
+ output_tau_betas.append([b, self._tau[b]])
966
+
967
+ return output_alphas, output_tau_betas
968
+
969
+ def set_value_restrictions(self, *value_restrictions):
970
+ r"""
971
+ Restrict the set of possible values `s(a)` for a given element
972
+ `a`.
973
+
974
+ .. WARNING::
975
+
976
+ Any restriction imposed by a previous invocation of
977
+ :meth:`set_value_restrictions` will be overwritten!
978
+
979
+ INPUT:
980
+
981
+ - ``value_restrictions`` -- one or more pairs `(a\in A, \tilde
982
+ Z\subseteq Z)`
983
+
984
+ EXAMPLES:
985
+
986
+ We may want to restrict the value of a given element to a
987
+ single or multiple values. We do not require that the
988
+ specified values are in the image of `\tau`. In some
989
+ cases, the restriction may not be able to provide a better
990
+ solution, as for size 3 in the following example. ::
991
+
992
+ sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
993
+ sage: tau = Permutation.longest_increasing_subsequence_length
994
+ sage: bij = Bijectionist(A, B, tau)
995
+ sage: bij.set_statistics((len, len))
996
+ sage: bij.set_value_restrictions((Permutation([1, 2]), [1]),
997
+ ....: (Permutation([3, 2, 1]), [2, 3, 4]))
998
+ sage: for sol in sorted(bij.solutions_iterator(), key=lambda d: sorted(d.items())):
999
+ ....: print(sol)
1000
+ {[]: 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}
1001
+ {[]: 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}
1002
+ {[]: 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}
1003
+ {[]: 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}
1004
+ {[]: 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}
1005
+ {[]: 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}
1006
+ {[]: 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}
1007
+ {[]: 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}
1008
+ {[]: 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}
1009
+ {[]: 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}
1010
+ {[]: 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}
1011
+ {[]: 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}
1012
+ {[]: 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}
1013
+ {[]: 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}
1014
+ {[]: 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}
1015
+ {[]: 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}
1016
+ {[]: 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}
1017
+ {[]: 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}
1018
+ {[]: 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}
1019
+ {[]: 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}
1020
+ {[]: 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}
1021
+ {[]: 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}
1022
+ {[]: 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}
1023
+ {[]: 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}
1024
+ {[]: 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}
1025
+
1026
+ However, an error occurs if the set of possible values is
1027
+ empty. In this example, the image of `\tau` under any
1028
+ legal bijection is disjoint to the specified values.
1029
+
1030
+ TESTS::
1031
+
1032
+ sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
1033
+ sage: tau = Permutation.longest_increasing_subsequence_length
1034
+ sage: bij = Bijectionist(A, B, tau)
1035
+ sage: bij.set_value_restrictions((Permutation([1, 2]), [4, 5]))
1036
+ sage: bij._compute_possible_block_values()
1037
+ Traceback (most recent call last):
1038
+ ...
1039
+ ValueError: no possible values found for singleton block [[1, 2]]
1040
+
1041
+ sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
1042
+ sage: tau = Permutation.longest_increasing_subsequence_length
1043
+ sage: bij = Bijectionist(A, B, tau)
1044
+ sage: bij.set_constant_blocks([[permutation for permutation in Permutations(n)] for n in range(4)])
1045
+ sage: bij.set_value_restrictions((Permutation([1, 2]), [4, 5]))
1046
+ sage: bij._compute_possible_block_values()
1047
+ Traceback (most recent call last):
1048
+ ...
1049
+ ValueError: no possible values found for block [[1, 2], [2, 1]]
1050
+
1051
+ sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
1052
+ sage: tau = Permutation.longest_increasing_subsequence_length
1053
+ sage: bij = Bijectionist(A, B, tau)
1054
+ sage: bij.set_value_restrictions(((1, 2), [4, 5, 6]))
1055
+ Traceback (most recent call last):
1056
+ ...
1057
+ AssertionError: element (1, 2) was not found in A
1058
+ """
1059
+ # it might be much cheaper to construct the sets as subsets
1060
+ # of _statistics_possible_values - however, we do not want to
1061
+ # insist that set_value_restrictions is called after
1062
+ # set_statistics
1063
+ self._bmilp = None
1064
+ set_Z = set(self._Z)
1065
+ self._restrictions_possible_values = {a: set_Z for a in self._A}
1066
+ for a, values in value_restrictions:
1067
+ assert a in self._A, f"element {a} was not found in A"
1068
+ self._restrictions_possible_values[a] = self._restrictions_possible_values[a].intersection(values)
1069
+
1070
+ def _compute_possible_block_values(self):
1071
+ r"""
1072
+ Update the dictionary of possible values of each block.
1073
+
1074
+ This has to be called whenever ``self._P`` was modified.
1075
+
1076
+ It raises a :exc:`ValueError`, if the restrictions on a
1077
+ block are contradictory.
1078
+
1079
+ TESTS::
1080
+
1081
+ sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
1082
+ sage: tau = Permutation.longest_increasing_subsequence_length
1083
+ sage: bij = Bijectionist(A, B, tau)
1084
+ sage: bij.set_value_restrictions((Permutation([1, 2]), [4, 5]))
1085
+ sage: bij._compute_possible_block_values()
1086
+ Traceback (most recent call last):
1087
+ ...
1088
+ ValueError: no possible values found for singleton block [[1, 2]]
1089
+ """
1090
+ self._possible_block_values = {} # P -> Power(Z)
1091
+ for p, block in self._P.root_to_elements_dict().items():
1092
+ sets = ([self._restrictions_possible_values[a] for a in block]
1093
+ + [self._statistics_possible_values[a] for a in block])
1094
+ self._possible_block_values[p] = _non_copying_intersection(sets)
1095
+ if not self._possible_block_values[p]:
1096
+ if len(block) == 1:
1097
+ raise ValueError(f"no possible values found for singleton block {block}")
1098
+ raise ValueError(f"no possible values found for block {block}")
1099
+
1100
+ def set_distributions(self, *elements_distributions):
1101
+ r"""
1102
+ Specify the distribution of `s` for a subset of elements.
1103
+
1104
+ .. WARNING::
1105
+
1106
+ Any restriction imposed by a previous invocation of
1107
+ :meth:`set_distributions` will be overwritten!
1108
+
1109
+ INPUT:
1110
+
1111
+ - one or more pairs of `(\tilde A, \tilde Z)`, where `\tilde
1112
+ A\subseteq A` and `\tilde Z` is a list of values in `Z` of
1113
+ the same size as `\tilde A`
1114
+
1115
+ This method specifies that `\{s(a) | a\in\tilde A\}` equals
1116
+ `\tilde Z` as a multiset for each of the pairs.
1117
+
1118
+ When specifying several distributions, the subsets of `A` do
1119
+ not have to be disjoint.
1120
+
1121
+ ALGORITHM:
1122
+
1123
+ We add
1124
+
1125
+ .. MATH::
1126
+
1127
+ \sum_{a\in\tilde A} x_{p(a), z}t^z = \sum_{z\in\tilde Z} t^z,
1128
+
1129
+ where `p(a)` is the block containing `a`, for each given
1130
+ distribution as a vector equation to the MILP.
1131
+
1132
+ EXAMPLES::
1133
+
1134
+ sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
1135
+ sage: tau = Permutation.longest_increasing_subsequence_length
1136
+ sage: bij = Bijectionist(A, B, tau)
1137
+ sage: bij.set_statistics((len, len))
1138
+ sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [1, 3]))
1139
+ sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
1140
+ ....: print(sol)
1141
+ {[]: 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}
1142
+ {[]: 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}
1143
+ {[]: 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}
1144
+ {[]: 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}
1145
+
1146
+ sage: bij.constant_blocks(optimal=True)
1147
+ {{[2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]}}
1148
+ sage: sorted(bij.minimal_subdistributions_blocks_iterator(), key=lambda d: (len(d[0]), d[0]))
1149
+ [([[]], [0]),
1150
+ ([[1]], [1]),
1151
+ ([[2, 1, 3]], [2]),
1152
+ ([[1, 2], [2, 1]], [1, 2]),
1153
+ ([[1, 2, 3], [1, 3, 2]], [1, 3])]
1154
+
1155
+ We may also specify multiple, possibly overlapping distributions::
1156
+
1157
+ sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [1, 3]),
1158
+ ....: ([Permutation([1, 3, 2]), Permutation([3, 2, 1]),
1159
+ ....: Permutation([2, 1, 3])], [1, 2, 2]))
1160
+ sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
1161
+ ....: print(sol)
1162
+ {[]: 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}
1163
+ {[]: 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}
1164
+
1165
+ sage: bij.constant_blocks(optimal=True)
1166
+ {{[1], [1, 3, 2]}, {[2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]}}
1167
+ sage: sorted(bij.minimal_subdistributions_blocks_iterator(), key=lambda d: (len(d[0]), d[0]))
1168
+ [([[]], [0]),
1169
+ ([[1]], [1]),
1170
+ ([[1, 2, 3]], [3]),
1171
+ ([[2, 3, 1]], [2]),
1172
+ ([[1, 2], [2, 1]], [1, 2])]
1173
+
1174
+ TESTS:
1175
+
1176
+ Because of the current implementation of the output calculation, we do
1177
+ not improve our solution if we do not gain any unique solutions::
1178
+
1179
+ sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
1180
+ sage: tau = Permutation.longest_increasing_subsequence_length
1181
+ sage: bij = Bijectionist(A, B, tau)
1182
+ sage: bij.set_statistics((len, len))
1183
+ sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [2, 3]))
1184
+ sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
1185
+ ....: print(sol)
1186
+ {[]: 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}
1187
+ {[]: 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}
1188
+ {[]: 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}
1189
+ {[]: 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}
1190
+ {[]: 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}
1191
+ {[]: 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}
1192
+ {[]: 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}
1193
+ {[]: 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}
1194
+ {[]: 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}
1195
+ {[]: 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}
1196
+ {[]: 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}
1197
+ {[]: 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}
1198
+ {[]: 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}
1199
+ {[]: 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}
1200
+ {[]: 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}
1201
+ {[]: 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}
1202
+
1203
+ Another example with statistics::
1204
+
1205
+ sage: bij = Bijectionist(A, B, tau)
1206
+ sage: def alpha(p): return p(1) if len(p) > 0 else 0
1207
+ sage: def beta(p): return p(1) if len(p) > 0 else 0
1208
+ sage: bij.set_statistics((alpha, beta), (len, len))
1209
+ sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
1210
+ ....: print(sol)
1211
+ {[]: 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}
1212
+ {[]: 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}
1213
+ {[]: 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}
1214
+ {[]: 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}
1215
+
1216
+ The solution above is not unique. We can add a feasible distribution to force uniqueness::
1217
+
1218
+ sage: bij = Bijectionist(A, B, tau)
1219
+ sage: bij.set_statistics((alpha, beta), (len, len))
1220
+ sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([3, 2, 1])], [1, 3]))
1221
+ sage: for sol in bij.solutions_iterator():
1222
+ ....: print(sol)
1223
+ {[]: 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}
1224
+
1225
+ Let us try to add a distribution that cannot be satisfied,
1226
+ because there is no solution where a permutation that starts
1227
+ with 1 is mapped onto 1::
1228
+
1229
+ sage: bij = Bijectionist(A, B, tau)
1230
+ sage: bij.set_statistics((alpha, beta), (len, len))
1231
+ sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [1, 3]))
1232
+ sage: list(bij.solutions_iterator())
1233
+ []
1234
+
1235
+ The specified elements have to be in `A` and have to be of the same size::
1236
+
1237
+ sage: bij = Bijectionist(A, B, tau)
1238
+ sage: bij.set_statistics((len, len))
1239
+ sage: bij.set_distributions(([Permutation([1, 2, 3, 4])], [1]))
1240
+ Traceback (most recent call last):
1241
+ ...
1242
+ ValueError: element [1, 2, 3, 4] was not found in A
1243
+ sage: bij.set_distributions(([Permutation([1, 2, 3])], [-1]))
1244
+ Traceback (most recent call last):
1245
+ ...
1246
+ ValueError: value -1 was not found in tau(A)
1247
+
1248
+ Note that the same error occurs when an element that is not the first element of the list is
1249
+ not in `A`.
1250
+ """
1251
+ self._bmilp = None
1252
+ for tA, tZ in elements_distributions:
1253
+ assert len(tA) == len(tZ), f"{tA} and {tZ} are not of the same size!"
1254
+ for a, z in zip(tA, tZ):
1255
+ if a not in self._A:
1256
+ raise ValueError(f"element {a} was not found in A")
1257
+ if z not in self._Z:
1258
+ raise ValueError(f"value {z} was not found in tau(A)")
1259
+ self._elements_distributions = tuple(elements_distributions)
1260
+
1261
+ def set_intertwining_relations(self, *pi_rho):
1262
+ r"""
1263
+ Add restrictions of the form `s(\pi(a_1,\dots, a_k)) =
1264
+ \rho(s(a_1),\dots, s(a_k))`.
1265
+
1266
+ .. WARNING::
1267
+
1268
+ Any restriction imposed by a previous invocation of
1269
+ :meth:`set_intertwining_relations` will be overwritten!
1270
+
1271
+ INPUT:
1272
+
1273
+ - ``pi_rho`` -- one or more tuples `(k, \pi: A^k\to A, \rho:
1274
+ Z^k\to Z, \tilde A)` where `\tilde A` (optional) is a
1275
+ `k`-ary function that returns true if and only if a
1276
+ `k`-tuple of objects in `A` is in the domain of `\pi`
1277
+
1278
+ ALGORITHM:
1279
+
1280
+ The relation
1281
+
1282
+ .. MATH::
1283
+
1284
+ s(\pi(a_1,\dots, a_k)) = \rho(s(a_1),\dots, s(a_k))
1285
+
1286
+ for each pair `(\pi, \rho)` implies immediately that
1287
+ `s(\pi(a_1,\dots, a_k))` only depends on the blocks of
1288
+ `a_1,\dots, a_k`.
1289
+
1290
+ The MILP formulation is as follows. Let `a_1,\dots,a_k \in
1291
+ A` and let `a = \pi(a_1,\dots,a_k)`. Let `z_1,\dots,z_k \in
1292
+ Z` and let `z = \rho(z_1,\dots,z_k)`. Suppose that `a_i\in
1293
+ p_i` for all `i` and that `a\in p`.
1294
+
1295
+ We then want to model the implication
1296
+
1297
+ .. MATH::
1298
+
1299
+ x_{p_1, z_1} = 1,\dots, x_{p_k, z_k} = 1 \Rightarrow x_{p, z} = 1.
1300
+
1301
+ We achieve this by requiring
1302
+
1303
+ .. MATH::
1304
+
1305
+ x_{p, z}\geq 1 - k + \sum_{i=1}^k x_{p_i, z_i}.
1306
+
1307
+ Note that `z` must be a possible value of `p` and each `z_i`
1308
+ must be a possible value of `p_i`.
1309
+
1310
+ EXAMPLES:
1311
+
1312
+ We can concatenate two permutations by increasing the values
1313
+ of the second permutation by the length of the first
1314
+ permutation::
1315
+
1316
+ sage: def concat(p1, p2): return Permutation(p1 + [i + len(p1) for i in p2])
1317
+
1318
+ We may be interested in statistics on permutations which are
1319
+ equidistributed with the number of fixed points, such that
1320
+ concatenating permutations corresponds to adding statistic
1321
+ values::
1322
+
1323
+ sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
1324
+ sage: bij = Bijectionist(A, B, Permutation.number_of_fixed_points)
1325
+ sage: bij.set_statistics((len, len))
1326
+ sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
1327
+ ....: print(solution)
1328
+ ...
1329
+ {[]: 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}
1330
+ ...
1331
+ {[]: 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}
1332
+ ...
1333
+
1334
+ sage: bij.set_intertwining_relations((2, concat, lambda x, y: x + y))
1335
+ sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
1336
+ ....: print(solution)
1337
+ {[]: 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}
1338
+ {[]: 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}
1339
+ {[]: 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}
1340
+
1341
+ The domain of the composition may be restricted. E.g., if we
1342
+ concatenate only permutations starting with a 1, we obtain
1343
+ fewer forced elements::
1344
+
1345
+ sage: in_domain = lambda p1, p2: (not p1 or p1(1) == 1) and (not p2 or p2(1) == 1)
1346
+ sage: bij.set_intertwining_relations((2, concat, lambda x, y: x + y, in_domain))
1347
+ sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
1348
+ ....: print(solution)
1349
+ {[]: 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}
1350
+ {[]: 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}
1351
+ {[]: 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}
1352
+ {[]: 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}
1353
+ {[]: 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}
1354
+ {[]: 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}
1355
+ {[]: 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}
1356
+ {[]: 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}
1357
+ {[]: 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}
1358
+ {[]: 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}
1359
+
1360
+ We can also restrict according to several composition
1361
+ functions. For example, we may additionally concatenate
1362
+ permutations by incrementing the elements of the first::
1363
+
1364
+ sage: skew_concat = lambda p1, p2: Permutation([i + len(p2) for i in p1] + list(p2))
1365
+ sage: bij.set_intertwining_relations((2, skew_concat, lambda x, y: x + y))
1366
+ sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
1367
+ ....: print(solution)
1368
+ {[]: 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}
1369
+ {[]: 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}
1370
+ {[]: 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}
1371
+
1372
+ However, this yields no solution::
1373
+
1374
+ sage: bij.set_intertwining_relations((2, concat, lambda x, y: x + y), (2, skew_concat, lambda x, y: x + y))
1375
+ sage: list(bij.solutions_iterator())
1376
+ []
1377
+ """
1378
+ self._bmilp = None
1379
+ Pi_Rho = namedtuple("Pi_Rho", "numargs pi rho domain")
1380
+ self._pi_rho = []
1381
+
1382
+ for pi_rho_tuple in pi_rho:
1383
+ if len(pi_rho_tuple) == 3:
1384
+ k, pi, rho = pi_rho_tuple
1385
+ domain = None
1386
+ else:
1387
+ k, pi, rho, domain = pi_rho_tuple
1388
+
1389
+ self._pi_rho.append(Pi_Rho(numargs=k, pi=pi, rho=rho, domain=domain))
1390
+
1391
+ set_semi_conjugacy = set_intertwining_relations
1392
+
1393
+ def set_quadratic_relation(self, *phi_psi):
1394
+ r"""
1395
+ Add restrictions of the form `s\circ\psi\circ s = \phi`.
1396
+
1397
+ INPUT:
1398
+
1399
+ - ``phi_psi`` -- (optional) a list of pairs `(\phi, \rho)` where `\phi:
1400
+ A\to Z` and `\psi: Z\to A`
1401
+
1402
+ ALGORITHM:
1403
+
1404
+ We add
1405
+
1406
+ .. MATH::
1407
+
1408
+ x_{p(a), z} = x_{p(\psi(z)), \phi(a)}
1409
+
1410
+ for `a\in A` and `z\in Z` to the MILP, where `\phi:A\to Z`
1411
+ and `\psi:Z\to A`. Note that, in particular, `\phi` must be
1412
+ constant on blocks.
1413
+
1414
+ EXAMPLES::
1415
+
1416
+ sage: A = B = DyckWords(3)
1417
+ sage: bij = Bijectionist(A, B)
1418
+ sage: bij.set_statistics((lambda D: D.number_of_touch_points(), lambda D: D.number_of_initial_rises()))
1419
+ sage: ascii_art(sorted(bij.minimal_subdistributions_iterator()))
1420
+ [ ( [ /\ ] )
1421
+ [ ( [ / \ ] ) ( [ /\ /\ ] [ /\ /\/\ ] )
1422
+ [ ( [ /\/\/\ ], [ / \ ] ), ( [ /\/ \, / \/\ ], [ / \/\, / \ ] ),
1423
+ <BLANKLINE>
1424
+ ( [ /\ ] ) ]
1425
+ ( [ /\/\ / \ ] [ /\ ] ) ]
1426
+ ( [ / \, / \ ], [ /\/\/\, /\/ \ ] ) ]
1427
+ sage: bij.set_quadratic_relation((lambda D: D, lambda D: D))
1428
+ sage: ascii_art(sorted(bij.minimal_subdistributions_iterator()))
1429
+ [ ( [ /\ ] )
1430
+ [ ( [ / \ ] ) ( [ /\ ] [ /\/\ ] )
1431
+ [ ( [ /\/\/\ ], [ / \ ] ), ( [ /\/ \ ], [ / \ ] ),
1432
+ <BLANKLINE>
1433
+ <BLANKLINE>
1434
+ ( [ /\ ] [ /\ ] ) ( [ /\/\ ] [ /\ ] )
1435
+ ( [ / \/\ ], [ / \/\ ] ), ( [ / \ ], [ /\/ \ ] ),
1436
+ <BLANKLINE>
1437
+ ( [ /\ ] ) ]
1438
+ ( [ / \ ] ) ]
1439
+ ( [ / \ ], [ /\/\/\ ] ) ]
1440
+ """
1441
+ self._bmilp = None
1442
+ self._phi_psi = phi_psi
1443
+
1444
+ def set_homomesic(self, Q):
1445
+ """
1446
+ Assert that the average of `s` on each block of `Q` is
1447
+ constant.
1448
+
1449
+ INPUT:
1450
+
1451
+ - ``Q`` -- set partition of ``A``
1452
+
1453
+ EXAMPLES::
1454
+
1455
+ sage: A = B = [1,2,3]
1456
+ sage: bij = Bijectionist(A, B, lambda b: b % 3)
1457
+ sage: bij.set_homomesic([[1,2], [3]])
1458
+ sage: list(bij.solutions_iterator())
1459
+ [{1: 2, 2: 0, 3: 1}, {1: 0, 2: 2, 3: 1}]
1460
+ """
1461
+ self._bmilp = None
1462
+ if Q is None:
1463
+ self._Q = None
1464
+ else:
1465
+ self._Q = SetPartition(Q)
1466
+ assert self._Q in SetPartitions(self._A), f"{Q} must be a set partition of A"
1467
+
1468
+ def _forced_constant_blocks(self):
1469
+ r"""
1470
+ Modify current partition into blocks to the coarsest possible
1471
+ one, meaning that after calling this function for every two
1472
+ distinct blocks `p_1`, `p_2` there exists a solution `s` with
1473
+ `s(p_1)\neq s(p_2)`.
1474
+
1475
+ ALGORITHM:
1476
+
1477
+ First we generate an initial solution. For all blocks i, j
1478
+ that have the same value under this initial solution, we add
1479
+ the constraint `x[i, z] + x[j, z] <= 1` for all possible
1480
+ values `z\in Z`. This constraint ensures that the `s` differs
1481
+ on the two blocks. If this modified problem does not have a
1482
+ solution, we know that the two blocks always have the same
1483
+ value and join them. Then we save all values of this new
1484
+ solution and continue looking at pairs of blocks that had the
1485
+ same value under all calculated solutions, until no blocks
1486
+ can be joined anymore.
1487
+
1488
+ EXAMPLES:
1489
+
1490
+ The easiest example is given by a constant `tau`, so everything
1491
+ is forced to be the same value:
1492
+
1493
+ sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)]
1494
+ sage: bij = Bijectionist(A, B, lambda x: 0)
1495
+ sage: bij.constant_blocks()
1496
+ {}
1497
+ sage: bij.constant_blocks(optimal=True) # indirect doctest
1498
+ {{[], [1], [1, 2], [2, 1]}}
1499
+
1500
+ In this other example we look at permutations with length 2 and 3::
1501
+
1502
+ sage: N = 4
1503
+ sage: A = B = [permutation for n in range(2, N) for permutation in Permutations(n)]
1504
+ sage: def tau(p): return p[0] if len(p) else 0
1505
+ sage: add_n = lambda p1: Permutation(p1 + [1 + len(p1)])
1506
+ sage: add_1 = lambda p1: Permutation([1] + [1 + i for i in p1])
1507
+ sage: bij = Bijectionist(A, B, tau)
1508
+ sage: bij.set_intertwining_relations((1, add_n, lambda x: x + 1), (1, add_1, lambda x: x + 1))
1509
+ sage: bij.set_statistics((len, len))
1510
+
1511
+ sage: bij.constant_blocks()
1512
+ {}
1513
+ sage: bij.constant_blocks(optimal=True)
1514
+ {{[1, 3, 2], [2, 1, 3]}}
1515
+
1516
+ Indeed, ``[1,3,2]`` and ``[2,1,3]`` have the same value in
1517
+ all solutions, but different values are possible::
1518
+
1519
+ sage: pi1 = Permutation([1,3,2]); pi2 = Permutation([2,1,3]);
1520
+ sage: set([(solution[pi1], solution[pi2]) for solution in bij.solutions_iterator()])
1521
+ {(2, 2), (3, 3)}
1522
+
1523
+ Another example involving the cycle type of permutations::
1524
+
1525
+ sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
1526
+ sage: bij = Bijectionist(A, B, lambda x: x.cycle_type())
1527
+
1528
+ Let us require that each permutation has the same value as its inverse::
1529
+
1530
+ sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition
1531
+ sage: P = orbit_decomposition([permutation for n in range(4) for permutation in Permutations(n)], Permutation.inverse)
1532
+ sage: bij.set_constant_blocks(P)
1533
+ sage: bij.constant_blocks()
1534
+ {{[2, 3, 1], [3, 1, 2]}}
1535
+
1536
+ sage: def concat(p1, p2): return Permutation(p1 + [i + len(p1) for i in p2])
1537
+ sage: def union(p1, p2): return Partition(sorted(list(p1) + list(p2), reverse=True))
1538
+ sage: bij.set_intertwining_relations((2, concat, union))
1539
+
1540
+ In this case we do not discover constant blocks by looking at the intertwining_relations only::
1541
+
1542
+ sage: next(bij.solutions_iterator())
1543
+ ...
1544
+ sage: bij.constant_blocks()
1545
+ {{[2, 3, 1], [3, 1, 2]}}
1546
+
1547
+ sage: bij.constant_blocks(optimal=True)
1548
+ {{[1, 3, 2], [2, 1, 3], [3, 2, 1]}, {[2, 3, 1], [3, 1, 2]}}
1549
+
1550
+ TESTS::
1551
+
1552
+ sage: N = 4
1553
+ sage: A = B = [permutation for n in range(N + 1) for permutation in Permutations(n)]
1554
+ sage: def alpha1(p): return len(p.weak_excedences())
1555
+ sage: def alpha2(p): return len(p.fixed_points())
1556
+ sage: def beta1(p): return len(p.descents(final_descent=True)) if p else 0
1557
+ sage: def beta2(p): return len([e for (e, f) in zip(p, p[1:] + [0]) if e == f + 1])
1558
+ sage: tau = Permutation.longest_increasing_subsequence_length
1559
+ sage: def rotate_permutation(p):
1560
+ ....: cycle = Permutation(tuple(range(1, len(p) + 1)))
1561
+ ....: return Permutation([cycle.inverse()(p(cycle(i))) for i in range(1, len(p) + 1)])
1562
+ sage: bij = Bijectionist(A, B, tau)
1563
+ sage: bij.set_statistics((alpha1, beta1), (alpha2, beta2))
1564
+ sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition
1565
+ sage: bij.set_constant_blocks(orbit_decomposition(A, rotate_permutation))
1566
+ sage: P = bij.constant_blocks()
1567
+ sage: P = [sorted(p, key=lambda p: (len(p), p)) for p in P]
1568
+ sage: P = sorted(P, key=lambda p: (len(next(iter(p))), len(p)))
1569
+ sage: for p in P:
1570
+ ....: print(p)
1571
+ [[1, 3, 2], [2, 1, 3], [3, 2, 1]]
1572
+ [[1, 4, 3, 2], [3, 2, 1, 4]]
1573
+ [[2, 1, 4, 3], [4, 3, 2, 1]]
1574
+ [[1, 2, 4, 3], [1, 3, 2, 4], [2, 1, 3, 4], [4, 2, 3, 1]]
1575
+ [[1, 3, 4, 2], [2, 3, 1, 4], [2, 4, 3, 1], [3, 2, 4, 1]]
1576
+ [[1, 4, 2, 3], [3, 1, 2, 4], [4, 1, 3, 2], [4, 2, 1, 3]]
1577
+ [[2, 4, 1, 3], [3, 1, 4, 2], [3, 4, 2, 1], [4, 3, 1, 2]]
1578
+
1579
+ sage: P = bij.constant_blocks(optimal=True)
1580
+ sage: P = [sorted(p, key=lambda p: (len(p), p)) for p in P]
1581
+ sage: P = sorted(P, key=lambda p: (len(next(iter(p))), len(p)))
1582
+ sage: for p in P:
1583
+ ....: print(p)
1584
+ [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4]]
1585
+ [[1, 3, 2], [2, 1, 3], [3, 2, 1],
1586
+ [1, 2, 4, 3], [1, 3, 2, 4], [1, 3, 4, 2], [1, 4, 3, 2],
1587
+ [2, 1, 3, 4], [2, 1, 4, 3], [2, 3, 1, 4], [2, 3, 4, 1],
1588
+ [2, 4, 3, 1], [3, 2, 1, 4], [3, 2, 4, 1], [4, 2, 3, 1],
1589
+ [4, 3, 2, 1]]
1590
+ [[1, 4, 2, 3], [2, 4, 1, 3], [3, 1, 2, 4], [3, 1, 4, 2],
1591
+ [3, 4, 2, 1], [4, 1, 3, 2], [4, 2, 1, 3], [4, 3, 1, 2]]
1592
+
1593
+ The permutation `[2, 1]` is in none of these blocks::
1594
+
1595
+ sage: bij.set_constant_blocks(orbit_decomposition(A, rotate_permutation))
1596
+ sage: all(s[Permutation([2, 1])] == s[Permutation([1])] for s in bij.solutions_iterator())
1597
+ False
1598
+
1599
+ sage: all(s[Permutation([2, 1])] == s[Permutation([1, 3, 2])] for s in bij.solutions_iterator())
1600
+ False
1601
+
1602
+ sage: all(s[Permutation([2, 1])] == s[Permutation([1, 4, 2, 3])] for s in bij.solutions_iterator())
1603
+ False
1604
+
1605
+ sage: A = B = ["a", "b", "c", "d", "e", "f"]
1606
+ sage: tau = {"a": 1, "b": 1, "c": 3, "d": 4, "e": 5, "f": 6}.get
1607
+ sage: bij = Bijectionist(A, B, tau)
1608
+ sage: bij.set_distributions((["a", "b"], [1, 1]), (["c", "d", "e"], [3, 4, 5]))
1609
+ sage: bij.constant_blocks()
1610
+ {}
1611
+ sage: bij.constant_blocks(optimal=True)
1612
+ {{'a', 'b'}}
1613
+
1614
+ sage: A = B = ["a", "b", "c", "d", "e", "f"]
1615
+ sage: tau = {"a": 1, "b": 1, "c": 5, "d": 4, "e": 4, "f": 6}.get
1616
+ sage: bij = Bijectionist(A, B, tau)
1617
+ sage: bij.set_distributions((["a", "b"], [1, 1]), (["d", "e"], [4, 4]))
1618
+ sage: bij.constant_blocks(optimal=True)
1619
+ {{'a', 'b'}, {'d', 'e'}}
1620
+
1621
+ sage: A = B = ["a", "b", "c", "d"]
1622
+ sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2}.get
1623
+ sage: bij = Bijectionist(A, B, tau)
1624
+ sage: bij.constant_blocks(optimal=True)
1625
+ {}
1626
+ sage: bij.set_constant_blocks([["a", "b"]])
1627
+ sage: bij.constant_blocks()
1628
+ {{'a', 'b'}}
1629
+ sage: bij.constant_blocks(optimal=True)
1630
+ {{'a', 'b'}, {'c', 'd'}}
1631
+ """
1632
+ if self._bmilp is None:
1633
+ self._bmilp = _BijectionistMILP(self)
1634
+
1635
+ solution = next(self._bmilp.solutions_iterator(True, []))
1636
+ # multiple_preimages[tZ] are the blocks p which have the same
1637
+ # value tZ[i] in the i-th known solution
1638
+ multiple_preimages = {(z,): tP
1639
+ for z, tP in _invert_dict(solution).items()
1640
+ if len(tP) > 1}
1641
+
1642
+ # _P has to be copied to not mess with the solution process
1643
+ # since we do not want to regenerate the bmilp in each step,
1644
+ # so blocks have to stay consistent during the whole process
1645
+ tmp_P = copy(self._P)
1646
+
1647
+ # check whether blocks p1 and p2 can have different values,
1648
+ # if so return such a solution
1649
+ def different_values(p1, p2):
1650
+ tmp_constraints = [self._bmilp._x[p1, z] + self._bmilp._x[p2, z] <= 1
1651
+ for z in self._possible_block_values[p1]
1652
+ if z in self._possible_block_values[p2]]
1653
+ return next(self._bmilp.solutions_iterator(True, tmp_constraints))
1654
+
1655
+ # try to find a pair of blocks having the same value on all
1656
+ # known solutions, and a solution such that the values are
1657
+ # different on this solution
1658
+ def merge_until_split():
1659
+ for tZ in list(multiple_preimages):
1660
+ tP = multiple_preimages[tZ]
1661
+ for i2 in range(len(tP) - 1, -1, -1):
1662
+ for i1 in range(i2):
1663
+ try:
1664
+ solution = different_values(tP[i1], tP[i2])
1665
+ except StopIteration:
1666
+ tmp_P.union(tP[i1], tP[i2])
1667
+ if len(multiple_preimages[tZ]) == 2:
1668
+ del multiple_preimages[tZ]
1669
+ else:
1670
+ tP.remove(tP[i2])
1671
+ break # skip all pairs (i, j) containing i2
1672
+ return solution
1673
+
1674
+ while True:
1675
+ solution = merge_until_split()
1676
+ if solution is None:
1677
+ self._P = tmp_P
1678
+ # recreate the MILP
1679
+ self._bmilp = _BijectionistMILP(self,
1680
+ self._bmilp._solution_cache)
1681
+ return
1682
+
1683
+ updated_multiple_preimages = defaultdict(list)
1684
+ for tZ, tP in multiple_preimages.items():
1685
+ for p in tP:
1686
+ updated_multiple_preimages[tZ + (solution[p],)].append(p)
1687
+ multiple_preimages = updated_multiple_preimages
1688
+
1689
+ def possible_values(self, p=None, optimal=False):
1690
+ r"""
1691
+ Return for each block the values of `s` compatible with the
1692
+ imposed restrictions.
1693
+
1694
+ INPUT:
1695
+
1696
+ - ``p`` -- (optional) a block of `P`, or an element of a
1697
+ block of `P`, or a list of these
1698
+
1699
+ - ``optimal`` -- boolean (default: ``False``); whether or not to
1700
+ compute the minimal possible set of statistic values
1701
+
1702
+ .. NOTE::
1703
+
1704
+ Computing the minimal possible set of statistic values
1705
+ may be computationally expensive.
1706
+
1707
+ .. TODO::
1708
+
1709
+ currently, calling this method with ``optimal=True`` does
1710
+ not update the internal dictionary, because this would
1711
+ interfere with the variables of the MILP.
1712
+
1713
+ EXAMPLES::
1714
+
1715
+ sage: A = B = ["a", "b", "c", "d"]
1716
+ sage: tau = {"a": 1, "b": 1, "c": 1, "d": 2}.get
1717
+ sage: bij = Bijectionist(A, B, tau)
1718
+ sage: bij.set_constant_blocks([["a", "b"]])
1719
+ sage: bij.possible_values(A)
1720
+ {'a': {1, 2}, 'b': {1, 2}, 'c': {1, 2}, 'd': {1, 2}}
1721
+ sage: bij.possible_values(A, optimal=True)
1722
+ {'a': {1}, 'b': {1}, 'c': {1, 2}, 'd': {1, 2}}
1723
+
1724
+ The internal dictionary is not updated::
1725
+
1726
+ sage: bij.possible_values(A)
1727
+ {'a': {1, 2}, 'b': {1, 2}, 'c': {1, 2}, 'd': {1, 2}}
1728
+
1729
+ TESTS::
1730
+
1731
+ sage: A = B = ["a", "b", "c", "d"]
1732
+ sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2}.get
1733
+ sage: bij = Bijectionist(A, B, tau)
1734
+ sage: bij.set_constant_blocks([["a", "b"]])
1735
+
1736
+ Test if all formats are really possible::
1737
+
1738
+ sage: bij.possible_values(p='a')
1739
+ {'a': {1, 2}, 'b': {1, 2}}
1740
+ sage: bij.possible_values(p=["a", "b"])
1741
+ {'a': {1, 2}, 'b': {1, 2}}
1742
+ sage: bij.possible_values(p=[["a", "b"]])
1743
+ {'a': {1, 2}, 'b': {1, 2}}
1744
+ sage: bij.possible_values(p=[["a", "b"], ["c"]])
1745
+ {'a': {1, 2}, 'b': {1, 2}, 'c': {1, 2}}
1746
+
1747
+ Test an unfeasible problem::
1748
+
1749
+ sage: A = B = 'ab'
1750
+ sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2)
1751
+ sage: bij.set_constant_blocks([['a', 'b']])
1752
+ sage: bij.possible_values(p='a')
1753
+ {'a': {0, 1}, 'b': {0, 1}}
1754
+ sage: bij.possible_values(p='a', optimal=True)
1755
+ {'a': set(), 'b': set()}
1756
+ """
1757
+ # convert input to set of block representatives
1758
+ blocks = set()
1759
+ if p in self._A:
1760
+ blocks.add(self._P.find(p))
1761
+ elif isinstance(p, list): # TODO: this looks very brittle
1762
+ for p1 in p:
1763
+ if p1 in self._A:
1764
+ blocks.add(self._P.find(p1))
1765
+ elif isinstance(p1, list):
1766
+ for p2 in p1:
1767
+ blocks.add(self._P.find(p2))
1768
+
1769
+ if optimal:
1770
+ if self._bmilp is None:
1771
+ self._bmilp = _BijectionistMILP(self)
1772
+ bmilp = self._bmilp
1773
+ solutions = defaultdict(set)
1774
+ try:
1775
+ solution = next(bmilp.solutions_iterator(True, []))
1776
+ except StopIteration:
1777
+ pass
1778
+ else:
1779
+ for p, z in solution.items():
1780
+ solutions[p].add(z)
1781
+ for p in blocks:
1782
+ tmp_constraints = [bmilp._x[p, z] == 0 for z in solutions[p]]
1783
+ while True:
1784
+ try:
1785
+ solution = next(bmilp.solutions_iterator(True, tmp_constraints))
1786
+ except StopIteration:
1787
+ break
1788
+ for p0, z in solution.items():
1789
+ solutions[p0].add(z)
1790
+ # veto new value and try again
1791
+ tmp_constraints.append(bmilp._x[p, solution[p]] == 0)
1792
+
1793
+ # create dictionary to return
1794
+ possible_values = {}
1795
+ for p in blocks:
1796
+ for a in self._P.root_to_elements_dict()[p]:
1797
+ possible_values[a] = solutions[p]
1798
+ else:
1799
+ # create dictionary to return
1800
+ if self._possible_block_values is None:
1801
+ self._compute_possible_block_values()
1802
+ possible_values = {}
1803
+ for p in blocks:
1804
+ for a in self._P.root_to_elements_dict()[p]:
1805
+ possible_values[a] = self._possible_block_values[p]
1806
+
1807
+ return possible_values
1808
+
1809
+ def minimal_subdistributions_iterator(self):
1810
+ r"""
1811
+ Return all minimal subsets `\tilde A` of `A`
1812
+ together with submultisets `\tilde Z` with `s(\tilde A) =
1813
+ \tilde Z` as multisets.
1814
+
1815
+ EXAMPLES::
1816
+
1817
+ sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)]
1818
+ sage: bij = Bijectionist(A, B, len)
1819
+ sage: bij.set_statistics((len, len))
1820
+ sage: for sol in bij.solutions_iterator():
1821
+ ....: print(sol)
1822
+ {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 2}
1823
+ sage: sorted(bij.minimal_subdistributions_iterator())
1824
+ [([[]], [0]), ([[1]], [1]), ([[1, 2]], [2]), ([[2, 1]], [2])]
1825
+
1826
+ Another example::
1827
+
1828
+ sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)]
1829
+ sage: def tau(D): return D.number_of_touch_points()
1830
+ sage: bij = Bijectionist(A, B, tau)
1831
+ sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength()))
1832
+ sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
1833
+ ....: print(solution)
1834
+ {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 1, [1, 1, 0, 0]: 2}
1835
+ {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1}
1836
+ sage: for subdistribution in bij.minimal_subdistributions_iterator():
1837
+ ....: print(subdistribution)
1838
+ ([[]], [0])
1839
+ ([[1, 0]], [1])
1840
+ ([[1, 0, 1, 0], [1, 1, 0, 0]], [1, 2])
1841
+
1842
+ An example with two elements of the same block in a subdistribution::
1843
+
1844
+ sage: A = B = ["a", "b", "c", "d", "e"]
1845
+ sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2, "e": 3}.get
1846
+ sage: bij = Bijectionist(A, B, tau)
1847
+ sage: bij.set_constant_blocks([["a", "b"]])
1848
+ sage: bij.set_value_restrictions(("a", [1, 2]))
1849
+ sage: bij.constant_blocks(optimal=True)
1850
+ {{'a', 'b'}}
1851
+ sage: list(bij.minimal_subdistributions_iterator())
1852
+ [(['a', 'b', 'c', 'd', 'e'], [1, 1, 2, 2, 3])]
1853
+ """
1854
+ # see
1855
+ # https://mathoverflow.net/questions/406751/find-a-subdistribution/406975
1856
+ # and
1857
+ # https://gitlab.com/mantepse/bijection-tools/-/issues/29
1858
+
1859
+ minimal_subdistribution = MixedIntegerLinearProgram(maximization=False, solver=self._solver)
1860
+ D = minimal_subdistribution.new_variable(binary=True) # the subset of elements
1861
+ V = minimal_subdistribution.new_variable(integer=True) # the subdistribution
1862
+ minimal_subdistribution.set_objective(sum(D[a] for a in self._A))
1863
+ minimal_subdistribution.add_constraint(sum(D[a] for a in self._A) >= 1)
1864
+
1865
+ if self._bmilp is None:
1866
+ self._bmilp = _BijectionistMILP(self)
1867
+ s = next(self._bmilp.solutions_iterator(False, []))
1868
+ while True:
1869
+ for v in self._Z:
1870
+ minimal_subdistribution.add_constraint(sum(D[a] for a in self._A if s[a] == v) == V[v])
1871
+ try:
1872
+ minimal_subdistribution.solve()
1873
+ except MIPSolverException:
1874
+ return
1875
+ d = minimal_subdistribution.get_values(D, convert=bool, tolerance=0.1) # a dict from A to {0, 1}
1876
+ new_s = self._find_counterexample(self._A, s, d, False)
1877
+ if new_s is None:
1878
+ values = self._sorter["Z"](s[a] for a in self._A if d[a])
1879
+ yield ([a for a in self._A if d[a]], values)
1880
+
1881
+ # get all variables with value 1
1882
+ active_vars = [D[a] for a in self._A
1883
+ if minimal_subdistribution.get_values(D[a], convert=bool, tolerance=0.1)]
1884
+
1885
+ # add constraint that not all of these can be 1, thus vetoing
1886
+ # the current solution
1887
+ minimal_subdistribution.add_constraint(sum(active_vars) <= len(active_vars) - 1,
1888
+ name='veto')
1889
+ else:
1890
+ s = new_s
1891
+
1892
+ def _find_counterexample(self, P, s0, d, on_blocks):
1893
+ r"""
1894
+ Return a solution `s` such that ``d`` is not a subdistribution of
1895
+ `s0`.
1896
+
1897
+ INPUT:
1898
+
1899
+ - ``P`` -- the representatives of the blocks, or `A` if
1900
+ ``on_blocks`` is ``False``
1901
+
1902
+ - ``s0`` -- a solution
1903
+
1904
+ - ``d`` -- a subset of `A`, in the form of a dict from `A` to
1905
+ `\{0, 1\}`
1906
+
1907
+ - ``on_blocks`` -- whether to return the counterexample on
1908
+ blocks or on elements
1909
+
1910
+ EXAMPLES::
1911
+
1912
+ sage: A = B = ["a", "b", "c", "d", "e"]
1913
+ sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2, "e": 3}.get
1914
+ sage: bij = Bijectionist(A, B, tau)
1915
+ sage: bij.set_constant_blocks([["a", "b"]])
1916
+ sage: bij.set_value_restrictions(("a", [1, 2]))
1917
+ sage: next(bij.solutions_iterator())
1918
+ {'a': 1, 'b': 1, 'c': 2, 'd': 3, 'e': 2}
1919
+
1920
+ sage: s0 = {'a': 1, 'b': 1, 'c': 2, 'd': 3, 'e': 2}
1921
+ sage: d = {'a': 1, 'b': 0, 'c': 0, 'd': 0, 'e': 0}
1922
+ sage: bij._find_counterexample(bij._A, s0, d, False)
1923
+ {'a': 2, 'b': 2, 'c': 1, 'd': 3, 'e': 1}
1924
+ """
1925
+ bmilp = self._bmilp
1926
+ for z in self._Z:
1927
+ z_in_d_count = sum(d[p] for p in P if s0[p] == z)
1928
+ if not z_in_d_count:
1929
+ continue
1930
+
1931
+ # try to find a solution which has a different
1932
+ # subdistribution on d than s0
1933
+ z_in_d = sum(d[p] * bmilp._x[self._P.find(p), z]
1934
+ for p in P
1935
+ if z in self._possible_block_values[self._P.find(p)])
1936
+
1937
+ # it is sufficient to require that z occurs less often as
1938
+ # a value among {a | d[a] == 1} than it does in
1939
+ # z_in_d_count, because, if the distributions are
1940
+ # different, one such z must exist
1941
+ tmp_constraints = [z_in_d <= z_in_d_count - 1]
1942
+ try:
1943
+ solution = next(bmilp.solutions_iterator(on_blocks, tmp_constraints))
1944
+ return solution
1945
+ except StopIteration:
1946
+ pass
1947
+
1948
+ def minimal_subdistributions_blocks_iterator(self):
1949
+ r"""
1950
+ Return all representatives of minimal subsets `\tilde P`
1951
+ of `P` together with submultisets `\tilde Z`
1952
+ with `s(\tilde P) = \tilde Z` as multisets.
1953
+
1954
+ .. WARNING::
1955
+
1956
+ If there are several solutions with the same support
1957
+ (i.e., the sets of block representatives are the same),
1958
+ only one of these will be found, even if the
1959
+ distributions are different, see the doctest below. To
1960
+ find all solutions, use
1961
+ :meth:`minimal_subdistributions_iterator`, which is,
1962
+ however, computationally more expensive.
1963
+
1964
+ EXAMPLES::
1965
+
1966
+ sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)]
1967
+ sage: bij = Bijectionist(A, B, len)
1968
+ sage: bij.set_statistics((len, len))
1969
+ sage: for sol in bij.solutions_iterator():
1970
+ ....: print(sol)
1971
+ {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 2}
1972
+ sage: sorted(bij.minimal_subdistributions_blocks_iterator())
1973
+ [([[]], [0]), ([[1]], [1]), ([[1, 2]], [2]), ([[2, 1]], [2])]
1974
+
1975
+ Another example::
1976
+
1977
+ sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)]
1978
+ sage: def tau(D): return D.number_of_touch_points()
1979
+ sage: bij = Bijectionist(A, B, tau)
1980
+ sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength()))
1981
+ sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
1982
+ ....: print(solution)
1983
+ {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 1, [1, 1, 0, 0]: 2}
1984
+ {[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1}
1985
+ sage: for subdistribution in bij.minimal_subdistributions_blocks_iterator():
1986
+ ....: print(subdistribution)
1987
+ ([[]], [0])
1988
+ ([[1, 0]], [1])
1989
+ ([[1, 0, 1, 0], [1, 1, 0, 0]], [1, 2])
1990
+
1991
+ An example with two elements of the same block in a subdistribution::
1992
+
1993
+ sage: A = B = ["a", "b", "c", "d", "e"]
1994
+ sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2, "e": 3}.get
1995
+ sage: bij = Bijectionist(A, B, tau)
1996
+ sage: bij.set_constant_blocks([["a", "b"]])
1997
+ sage: bij.set_value_restrictions(("a", [1, 2]))
1998
+ sage: bij.constant_blocks(optimal=True)
1999
+ {{'a', 'b'}}
2000
+ sage: list(bij.minimal_subdistributions_blocks_iterator())
2001
+ [(['b', 'b', 'c', 'd', 'e'], [1, 1, 2, 2, 3])]
2002
+
2003
+ An example with overlapping minimal subdistributions::
2004
+
2005
+ sage: A = B = ["a", "b", "c", "d", "e"]
2006
+ sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2, "e": 3}.get
2007
+ sage: bij = Bijectionist(A, B, tau)
2008
+ sage: bij.set_distributions((["a", "b"], [1, 2]), (["a", "c", "d"], [1, 2, 3]))
2009
+ sage: sorted(bij.solutions_iterator(), key=lambda d: tuple(sorted(d.items())))
2010
+ [{'a': 1, 'b': 2, 'c': 2, 'd': 3, 'e': 1},
2011
+ {'a': 1, 'b': 2, 'c': 3, 'd': 2, 'e': 1},
2012
+ {'a': 2, 'b': 1, 'c': 1, 'd': 3, 'e': 2},
2013
+ {'a': 2, 'b': 1, 'c': 3, 'd': 1, 'e': 2}]
2014
+ sage: bij.constant_blocks(optimal=True)
2015
+ {{'a', 'e'}}
2016
+ sage: list(bij.minimal_subdistributions_blocks_iterator())
2017
+ [(['a', 'b'], [1, 2]), (['a', 'c', 'd'], [1, 2, 3])]
2018
+
2019
+ Fedor Petrov's example from https://mathoverflow.net/q/424187::
2020
+
2021
+ sage: A = B = ["a"+str(i) for i in range(1, 9)] + ["b"+str(i) for i in range(3, 9)] + ["d"]
2022
+ sage: tau = {b: 0 if i < 10 else 1 for i, b in enumerate(B)}.get
2023
+ sage: bij = Bijectionist(A, B, tau)
2024
+ sage: bij.set_constant_blocks([["a"+str(i), "b"+str(i)] for i in range(1, 9) if "b"+str(i) in A])
2025
+ sage: d = [0]*8+[1]*4
2026
+ sage: bij.set_distributions((A[:8] + A[8+2:-1], d), (A[:8] + A[8:-3], d))
2027
+ sage: sorted([s[a] for a in A] for s in bij.solutions_iterator())
2028
+ [[0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1],
2029
+ [0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0],
2030
+ [0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0],
2031
+ [0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0],
2032
+ [0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0],
2033
+ [1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0],
2034
+ [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0],
2035
+ [1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0],
2036
+ [1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0],
2037
+ [1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1],
2038
+ [1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]]
2039
+
2040
+ sage: sorted(bij.minimal_subdistributions_blocks_iterator()) # random
2041
+ [(['a1', 'a2', 'a3', 'a4', 'a5', 'a5', 'a6', 'a6', 'a7', 'a7', 'a8', 'a8'],
2042
+ [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1]),
2043
+ (['a3', 'a4', 'd'], [0, 0, 1]),
2044
+ (['a7', 'a8', 'd'], [0, 0, 1])]
2045
+
2046
+ The following solution is not found, because it happens to
2047
+ have the same support as the other::
2048
+
2049
+ sage: D = set(A).difference(['b7', 'b8', 'd'])
2050
+ sage: sorted(a.replace("b", "a") for a in D)
2051
+ ['a1', 'a2', 'a3', 'a3', 'a4', 'a4', 'a5', 'a5', 'a6', 'a6', 'a7', 'a8']
2052
+ sage: set(tuple(sorted(s[a] for a in D)) for s in bij.solutions_iterator())
2053
+ {(0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1)}
2054
+
2055
+ But it is, by design, included here::
2056
+
2057
+ sage: sorted(D) in [d for d, _ in bij.minimal_subdistributions_iterator()]
2058
+ True
2059
+ """
2060
+ # see
2061
+ # https://mathoverflow.net/questions/406751/find-a-subdistribution/406975
2062
+ # and
2063
+ # https://gitlab.com/mantepse/bijection-tools/-/issues/29
2064
+ # see https://mathoverflow.net/q/424187 for Fedor Petrov's example
2065
+
2066
+ minimal_subdistribution = MixedIntegerLinearProgram(maximization=False, solver=self._solver)
2067
+ D = minimal_subdistribution.new_variable(integer=True, nonnegative=True) # the submultiset of elements
2068
+ X = minimal_subdistribution.new_variable(binary=True) # the support of D
2069
+ V = minimal_subdistribution.new_variable(integer=True, nonnegative=True) # the subdistribution
2070
+ P = _disjoint_set_roots(self._P)
2071
+ minimal_subdistribution.set_objective(sum(D[p] for p in P))
2072
+ minimal_subdistribution.add_constraint(sum(D[p] for p in P) >= 1)
2073
+ for p in P:
2074
+ minimal_subdistribution.add_constraint(D[p] <= len(self._P.root_to_elements_dict()[p]))
2075
+ minimal_subdistribution.add_constraint(X[p] * len(self._P.root_to_elements_dict()[p]) >= D[p] >= X[p])
2076
+
2077
+ def add_counter_example_constraint(s):
2078
+ for v in self._Z:
2079
+ minimal_subdistribution.add_constraint(sum(D[p] for p in P
2080
+ if s[p] == v) == V[v])
2081
+
2082
+ if self._bmilp is None:
2083
+ self._bmilp = _BijectionistMILP(self)
2084
+
2085
+ s = next(self._bmilp.solutions_iterator(True, []))
2086
+ add_counter_example_constraint(s)
2087
+ while True:
2088
+ try:
2089
+ minimal_subdistribution.solve()
2090
+ except MIPSolverException:
2091
+ return
2092
+ d = minimal_subdistribution.get_values(D, convert=ZZ, tolerance=0.1) # a dict from P to multiplicities
2093
+ new_s = self._find_counterexample(P, s, d, True)
2094
+ if new_s is None:
2095
+ yield ([p for p in P for _ in range(ZZ(d[p]))],
2096
+ self._sorter["Z"](s[p]
2097
+ for p in P
2098
+ for _ in range(ZZ(d[p]))))
2099
+
2100
+ support = [X[p] for p in P if d[p]]
2101
+ # add constraint that the support is different
2102
+ minimal_subdistribution.add_constraint(sum(support) <= len(support) - 1,
2103
+ name='veto')
2104
+ else:
2105
+ s = new_s
2106
+ add_counter_example_constraint(s)
2107
+
2108
+ def _preprocess_intertwining_relations(self):
2109
+ r"""
2110
+ Make ``self._P`` be the finest set partition coarser
2111
+ than ``self._P`` such that composing elements preserves
2112
+ blocks.
2113
+
2114
+ Suppose that `p_1`, `p_2` are blocks of `P`, and `a_1, a'_1
2115
+ \in p_1` and `a_2, a'_2\in p_2`. Then,
2116
+
2117
+ .. MATH:
2118
+
2119
+ s(\pi(a_1, a_2))
2120
+ = \rho(s(a_1), s(a_2))
2121
+ = \rho(s(a'_1), s(a'_2))
2122
+ = s(\pi(a'_1, a'_2)).
2123
+
2124
+ Therefore, `\pi(a_1, a_2)` and `\pi(a'_1, a'_2)` are in the
2125
+ same block.
2126
+
2127
+ In other words, `s(\pi(a_1,\dots,a_k))` only depends on the
2128
+ blocks of `a_1,\dots,a_k`.
2129
+
2130
+ In particular, if `P` consists only if singletons, this
2131
+ method has no effect.
2132
+
2133
+ .. TODO::
2134
+
2135
+ it is not clear whether this method makes sense
2136
+
2137
+ EXAMPLES::
2138
+
2139
+ sage: A = B = 'abcd'
2140
+ sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2)
2141
+ sage: def pi(p1, p2): return 'abcdefgh'[A.index(p1) + A.index(p2)]
2142
+ sage: def rho(s1, s2): return (s1 + s2) % 2
2143
+ sage: bij.set_intertwining_relations((2, pi, rho))
2144
+ sage: bij._preprocess_intertwining_relations()
2145
+ sage: bij._P
2146
+ {{'a'}, {'b'}, {'c'}, {'d'}}
2147
+
2148
+ However, adding that ``'a'`` and ``'c'`` are in the same block,
2149
+ we can infer that also ``'b'`` and ``'d'`` are in the same
2150
+ block::
2151
+
2152
+ sage: bij.set_constant_blocks([['a', 'c']])
2153
+ sage: bij._P
2154
+ {{'a', 'c'}, {'b'}, {'d'}}
2155
+ sage: bij._preprocess_intertwining_relations()
2156
+ sage: bij._P
2157
+ {{'a', 'c'}, {'b', 'd'}}
2158
+
2159
+ Let a group act on permutations::
2160
+
2161
+ sage: A = B = Permutations(3)
2162
+ sage: bij = Bijectionist(A, B, lambda x: x[0])
2163
+ sage: bij.set_intertwining_relations((1, lambda pi: pi.reverse(), lambda z: z))
2164
+ sage: bij._preprocess_intertwining_relations()
2165
+ sage: bij._P
2166
+ {{[1, 2, 3]}, {[1, 3, 2]}, {[2, 1, 3]}, {[2, 3, 1]}, {[3, 1, 2]}, {[3, 2, 1]}}
2167
+
2168
+ Thus, in this case we do not detect the constant blocks::
2169
+
2170
+ sage: bij.constant_blocks(optimal=True)
2171
+ {{[1, 2, 3], [3, 2, 1]}, {[1, 3, 2], [2, 3, 1]}, {[2, 1, 3], [3, 1, 2]}}
2172
+ """
2173
+ A = self._A
2174
+ P = self._P
2175
+ images = defaultdict(set) # A^k -> A, a_1,...,a_k +-> {pi(a_1,...,a_k) for all pi}
2176
+ for pi_rho in self._pi_rho:
2177
+ for a_tuple in itertools.product(*([A] * pi_rho.numargs)):
2178
+ if pi_rho.domain is not None and not pi_rho.domain(*a_tuple):
2179
+ continue
2180
+ a = pi_rho.pi(*a_tuple)
2181
+ if a in A:
2182
+ images[a_tuple].add(a)
2183
+
2184
+ # merge blocks
2185
+ something_changed = True
2186
+ while something_changed:
2187
+ something_changed = False
2188
+ # collect (preimage, image) pairs by (representatives) of
2189
+ # the blocks of the elements of the preimage
2190
+ updated_images = defaultdict(set) # (p_1,...,p_k) to {a_1,....}
2191
+ for a_tuple, image_set in images.items():
2192
+ representatives = tuple(P.find(a) for a in a_tuple)
2193
+ updated_images[representatives].update(image_set)
2194
+
2195
+ # merge blocks
2196
+ for a_tuple, image_set in updated_images.items():
2197
+ image = image_set.pop()
2198
+ while image_set:
2199
+ P.union(image, image_set.pop())
2200
+ something_changed = True
2201
+ # we keep a representative
2202
+ image_set.add(image)
2203
+
2204
+ images = updated_images
2205
+
2206
+ def solutions_iterator(self):
2207
+ r"""
2208
+ An iterator over all solutions of the problem.
2209
+
2210
+ OUTPUT: an iterator over all possible mappings `s: A\to Z`
2211
+
2212
+ ALGORITHM:
2213
+
2214
+ We solve an integer linear program with a binary variable
2215
+ `x_{p, z}` for each partition block `p\in P` and each
2216
+ statistic value `z\in Z`:
2217
+
2218
+ - `x_{p, z} = 1` if and only if `s(a) = z` for all `a\in p`.
2219
+
2220
+ Then we add the constraint `\sum_{x\in V} x<|V|`, where `V`
2221
+ is the set containing all `x` with `x = 1`, that is, those
2222
+ indicator variables representing the current solution.
2223
+ Therefore, a solution of this new program must be different
2224
+ from all those previously obtained.
2225
+
2226
+ INTEGER LINEAR PROGRAM:
2227
+
2228
+ * Let `m_w(p)`, for a block `p` of `P`, be the multiplicity
2229
+ of the value `w` in `W` under `\alpha`, that is, the number
2230
+ of elements `a \in p` with `\alpha(a)=w`.
2231
+
2232
+ * Let `n_w(z)` be the number of elements `b \in B` with
2233
+ `\beta(b)=w` and `\tau(b)=z` for `w \in W`, `z \in Z`.
2234
+
2235
+ * Let `k` be the arity of a pair `(\pi, \rho)` in an
2236
+ intertwining relation.
2237
+
2238
+ and the following constraints:
2239
+
2240
+ * because every block is assigned precisely one value, for
2241
+ all `p\in P`,
2242
+
2243
+ .. MATH::
2244
+
2245
+ \sum_z x_{p, z} = 1.
2246
+
2247
+ * because the statistics `s` and `\tau` and also `\alpha` and
2248
+ `\beta` are equidistributed, for all `w\in W` and `z\in Z`,
2249
+
2250
+ .. MATH::
2251
+
2252
+ \sum_p m_w(p) x_{p, z} = n_w(z).
2253
+
2254
+ * for each intertwining relation `s(\pi(a_1,\dots, a_k)) =
2255
+ \rho(s(a_1),\dots, s(a_r))`, and for all `k`-combinations
2256
+ of blocks `p_i\in P` such that there exist `(a_1,\dots,
2257
+ a_k)\in p_1\times\dots\times p_k` with `\pi(a_1,\dots,
2258
+ a_k)\in W` and `z = \rho(z_1,\dots, z_k)`,
2259
+
2260
+ .. MATH::
2261
+
2262
+ x_{p, z} \geq 1-k + \sum_{i=1}^k x_{p_i, z_i}.
2263
+
2264
+ * for each distribution restriction, i.e. a set of elements
2265
+ `\tilde A` and a distribution of values given by integers
2266
+ `d_z` representing the multiplicity of each `z \in Z`, and
2267
+ `r_p = |p \cap\tilde A|` indicating the relative size of
2268
+ block `p` in the set of elements of the distribution,
2269
+
2270
+ .. MATH::
2271
+
2272
+ \sum_p r_p x_{p, z} = d_z.
2273
+
2274
+ EXAMPLES::
2275
+
2276
+ sage: A = B = 'abc'
2277
+ sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2, solver='GLPK')
2278
+ sage: next(bij.solutions_iterator())
2279
+ {'a': 0, 'b': 1, 'c': 0}
2280
+
2281
+ sage: list(bij.solutions_iterator())
2282
+ [{'a': 0, 'b': 1, 'c': 0},
2283
+ {'a': 1, 'b': 0, 'c': 0},
2284
+ {'a': 0, 'b': 0, 'c': 1}]
2285
+
2286
+ sage: N = 4
2287
+ sage: A = B = [permutation for n in range(N) for permutation in Permutations(n)]
2288
+
2289
+ Let `\tau` be the number of non-left-to-right-maxima of a
2290
+ permutation::
2291
+
2292
+ sage: def tau(pi):
2293
+ ....: pi = list(pi)
2294
+ ....: i = count = 0
2295
+ ....: for j in range(len(pi)):
2296
+ ....: if pi[j] > i:
2297
+ ....: i = pi[j]
2298
+ ....: else:
2299
+ ....: count += 1
2300
+ ....: return count
2301
+
2302
+ We look for a statistic which is constant on conjugacy classes::
2303
+
2304
+ sage: P = [list(a) for n in range(N) for a in Permutations(n).conjugacy_classes()]
2305
+
2306
+ sage: bij = Bijectionist(A, B, tau, solver='GLPK')
2307
+ sage: bij.set_statistics((len, len))
2308
+ sage: bij.set_constant_blocks(P)
2309
+ sage: for solution in bij.solutions_iterator():
2310
+ ....: print(solution)
2311
+ {[]: 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}
2312
+ {[]: 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}
2313
+
2314
+ Changing or re-setting problem parameters clears the internal
2315
+ cache. Setting the verbosity prints the MILP which is solved.::
2316
+
2317
+ sage: set_verbose(2)
2318
+ sage: bij.set_constant_blocks(P)
2319
+ sage: _ = list(bij.solutions_iterator())
2320
+ Constraints are:
2321
+ block []: 1 <= x_0 <= 1
2322
+ block [1]: 1 <= x_1 <= 1
2323
+ block [1, 2]: 1 <= x_2 + x_3 <= 1
2324
+ block [2, 1]: 1 <= x_4 + x_5 <= 1
2325
+ block [1, 2, 3]: 1 <= x_6 + x_7 + x_8 <= 1
2326
+ block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1
2327
+ block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1
2328
+ statistics: 1 <= x_0 <= 1
2329
+ statistics: 0 <= <= 0
2330
+ statistics: 0 <= <= 0
2331
+ statistics: 1 <= x_1 <= 1
2332
+ statistics: 0 <= <= 0
2333
+ statistics: 0 <= <= 0
2334
+ statistics: 1 <= x_2 + x_4 <= 1
2335
+ statistics: 1 <= x_3 + x_5 <= 1
2336
+ statistics: 0 <= <= 0
2337
+ statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1
2338
+ statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3
2339
+ statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2
2340
+ Variables are:
2341
+ x_0: s([]) = 0
2342
+ x_1: s([1]) = 0
2343
+ x_2: s([1, 2]) = 0
2344
+ x_3: s([1, 2]) = 1
2345
+ x_4: s([2, 1]) = 0
2346
+ x_5: s([2, 1]) = 1
2347
+ x_6: s([1, 2, 3]) = 0
2348
+ x_7: s([1, 2, 3]) = 1
2349
+ x_8: s([1, 2, 3]) = 2
2350
+ x_9: s([1, 3, 2]) = s([2, 1, 3]) = s([3, 2, 1]) = 0
2351
+ x_10: s([1, 3, 2]) = s([2, 1, 3]) = s([3, 2, 1]) = 1
2352
+ x_11: s([1, 3, 2]) = s([2, 1, 3]) = s([3, 2, 1]) = 2
2353
+ x_12: s([2, 3, 1]) = s([3, 1, 2]) = 0
2354
+ x_13: s([2, 3, 1]) = s([3, 1, 2]) = 1
2355
+ x_14: s([2, 3, 1]) = s([3, 1, 2]) = 2
2356
+ after vetoing
2357
+ Constraints are:
2358
+ block []: 1 <= x_0 <= 1
2359
+ block [1]: 1 <= x_1 <= 1
2360
+ block [1, 2]: 1 <= x_2 + x_3 <= 1
2361
+ block [2, 1]: 1 <= x_4 + x_5 <= 1
2362
+ block [1, 2, 3]: 1 <= x_6 + x_7 + x_8 <= 1
2363
+ block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1
2364
+ block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1
2365
+ statistics: 1 <= x_0 <= 1
2366
+ statistics: 0 <= <= 0
2367
+ statistics: 0 <= <= 0
2368
+ statistics: 1 <= x_1 <= 1
2369
+ statistics: 0 <= <= 0
2370
+ statistics: 0 <= <= 0
2371
+ statistics: 1 <= x_2 + x_4 <= 1
2372
+ statistics: 1 <= x_3 + x_5 <= 1
2373
+ statistics: 0 <= <= 0
2374
+ statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1
2375
+ statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3
2376
+ statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2
2377
+ veto: x_0 + x_1 + x_3 + x_4 + x_6 + x_10 + x_14 <= 6
2378
+ after vetoing
2379
+ Constraints are:
2380
+ block []: 1 <= x_0 <= 1
2381
+ block [1]: 1 <= x_1 <= 1
2382
+ block [1, 2]: 1 <= x_2 + x_3 <= 1
2383
+ block [2, 1]: 1 <= x_4 + x_5 <= 1
2384
+ block [1, 2, 3]: 1 <= x_6 + x_7 + x_8 <= 1
2385
+ block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1
2386
+ block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1
2387
+ statistics: 1 <= x_0 <= 1
2388
+ statistics: 0 <= <= 0
2389
+ statistics: 0 <= <= 0
2390
+ statistics: 1 <= x_1 <= 1
2391
+ statistics: 0 <= <= 0
2392
+ statistics: 0 <= <= 0
2393
+ statistics: 1 <= x_2 + x_4 <= 1
2394
+ statistics: 1 <= x_3 + x_5 <= 1
2395
+ statistics: 0 <= <= 0
2396
+ statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1
2397
+ statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3
2398
+ statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2
2399
+ veto: x_0 + x_1 + x_3 + x_4 + x_6 + x_10 + x_14 <= 6
2400
+ veto: x_0 + x_1 + x_2 + x_5 + x_6 + x_10 + x_14 <= 6
2401
+
2402
+ sage: set_verbose(0)
2403
+
2404
+ TESTS:
2405
+
2406
+ An unfeasible problem::
2407
+
2408
+ sage: A = ["a", "b", "c", "d"]; B = [1, 2, 3, 4]
2409
+ sage: bij = Bijectionist(A, B)
2410
+ sage: bij.set_value_restrictions(("a", [1, 2]), ("b", [1, 2]), ("c", [1, 3]), ("d", [2, 3]))
2411
+ sage: list(bij.solutions_iterator())
2412
+ []
2413
+
2414
+ Testing interactions between multiple instances using Fedor Petrov's example from https://mathoverflow.net/q/424187::
2415
+
2416
+ sage: A = B = ["a"+str(i) for i in range(1, 9)] + ["b"+str(i) for i in range(3, 9)] + ["d"]
2417
+ sage: tau = {b: 0 if i < 10 else 1 for i, b in enumerate(B)}.get
2418
+ sage: bij = Bijectionist(A, B, tau)
2419
+ sage: bij.set_constant_blocks([["a"+str(i), "b"+str(i)] for i in range(1, 9) if "b"+str(i) in A])
2420
+ sage: d = [0]*8+[1]*4
2421
+ sage: bij.set_distributions((A[:8] + A[8+2:-1], d), (A[:8] + A[8:-3], d))
2422
+ sage: iterator1 = bij.solutions_iterator()
2423
+ sage: iterator2 = bij.solutions_iterator()
2424
+
2425
+ Generate a solution in iterator1, iterator2 should generate the same solution and vice versa::
2426
+
2427
+ sage: s1_1 = next(iterator1)
2428
+ sage: s2_1 = next(iterator2)
2429
+ sage: s1_1 == s2_1
2430
+ True
2431
+ sage: s2_2 = next(iterator2)
2432
+ sage: s1_2 = next(iterator1)
2433
+ sage: s1_2 == s2_2
2434
+ True
2435
+
2436
+ Re-setting the distribution resets the cache, so a new
2437
+ iterator will generate the first solutions again, but the old
2438
+ iterator continues::
2439
+
2440
+ sage: bij.set_distributions((A[:8] + A[8+2:-1], d), (A[:8] + A[8:-3], d))
2441
+ sage: iterator3 = bij.solutions_iterator()
2442
+
2443
+ sage: s3_1 = next(iterator3)
2444
+ sage: s1_1 == s3_1
2445
+ True
2446
+
2447
+ sage: s1_3 = next(iterator1)
2448
+ sage: len(set([tuple(sorted(s.items())) for s in [s1_1, s1_2, s1_3]]))
2449
+ 3
2450
+ """
2451
+ if self._bmilp is None:
2452
+ self._bmilp = _BijectionistMILP(self)
2453
+ yield from self._bmilp.solutions_iterator(False, [])
2454
+
2455
+
2456
+ class _BijectionistMILP:
2457
+ r"""
2458
+ Wrapper class for the MixedIntegerLinearProgram (MILP).
2459
+
2460
+ This class is used to manage the MILP, add constraints, solve the
2461
+ problem and check for uniqueness of solution values.
2462
+ """
2463
+ def __init__(self, bijectionist: Bijectionist, solutions=None):
2464
+ r"""
2465
+ Initialize the mixed integer linear program.
2466
+
2467
+ INPUT:
2468
+
2469
+ - ``bijectionist`` -- an instance of :class:`Bijectionist`
2470
+
2471
+ - ``solutions`` -- (optional) a list of solutions of the
2472
+ problem, each provided as a dictionary mapping `(a, z)` to
2473
+ a boolean, such that at least one element from each block
2474
+ of `P` appears as `a`.
2475
+
2476
+ .. TODO::
2477
+
2478
+ it might be cleaner not to pass the full bijectionist
2479
+ instance, but only those attributes we actually use
2480
+
2481
+ TESTS::
2482
+
2483
+ sage: A = B = ["a", "b", "c", "d"]
2484
+ sage: bij = Bijectionist(A, B)
2485
+ sage: from sage.combinat.bijectionist import _BijectionistMILP
2486
+ sage: _BijectionistMILP(bij)
2487
+ <sage.combinat.bijectionist._BijectionistMILP object at ...>
2488
+ """
2489
+ # the attributes of the bijectionist class we actually use:
2490
+ # _possible_block_values
2491
+ # _elements_distributions
2492
+ # _W, _Z, _A, _B, _P, _alpha, _beta, _tau, _pi_rho, _phi_psi
2493
+ self._bijectionist = bijectionist
2494
+ # the variables of the MILP are indexed by pairs (p, z), for
2495
+ # p in _P and z an element of _posible_block_values[p].
2496
+ # Thus, _P and _posible_block_values have to be fixed before
2497
+ # creating the MILP.
2498
+ bijectionist._preprocess_intertwining_relations()
2499
+ bijectionist._compute_possible_block_values()
2500
+
2501
+ self.milp = MixedIntegerLinearProgram(solver=bijectionist._solver)
2502
+ self.milp.set_objective(None)
2503
+ indices = [(p, z)
2504
+ for p, tZ in bijectionist._possible_block_values.items()
2505
+ for z in tZ]
2506
+ self._x = self.milp.new_variable(binary=True, indices=indices)
2507
+
2508
+ tZ = bijectionist._possible_block_values
2509
+ P = bijectionist._P
2510
+ for p in _disjoint_set_roots(P):
2511
+ self.milp.add_constraint(sum(self._x[p, z] for z in tZ[p]) == 1,
2512
+ name=f"block {p}"[:50])
2513
+ self.add_alpha_beta_constraints()
2514
+ self.add_distribution_constraints()
2515
+ self.add_quadratic_relation_constraints()
2516
+ self.add_homomesic_constraints()
2517
+ self.add_intertwining_relation_constraints()
2518
+ if get_verbose() >= 2:
2519
+ self.show()
2520
+
2521
+ self._solution_cache = []
2522
+ if solutions is not None:
2523
+ for solution in solutions:
2524
+ self._add_solution({(P.find(a), z): value
2525
+ for (a, z), value in solution.items()})
2526
+
2527
+ def show(self, variables=True):
2528
+ r"""
2529
+ Print the constraints and variables of the MILP together
2530
+ with some explanations.
2531
+
2532
+ EXAMPLES::
2533
+
2534
+ sage: A = B = ["a", "b", "c"]
2535
+ sage: bij = Bijectionist(A, B, lambda x: A.index(x) % 2, solver='GLPK')
2536
+ sage: bij.set_constant_blocks([["a", "b"]])
2537
+ sage: next(bij.solutions_iterator())
2538
+ {'a': 0, 'b': 0, 'c': 1}
2539
+ sage: bij._bmilp.show()
2540
+ Constraints are:
2541
+ block a: 1 <= x_0 + x_1 <= 1
2542
+ block c: 1 <= x_2 + x_3 <= 1
2543
+ statistics: 2 <= 2 x_0 + x_2 <= 2
2544
+ statistics: 1 <= 2 x_1 + x_3 <= 1
2545
+ veto: x_0 + x_3 <= 1
2546
+ Variables are:
2547
+ x_0: s(a) = s(b) = 0
2548
+ x_1: s(a) = s(b) = 1
2549
+ x_2: s(c) = 0
2550
+ x_3: s(c) = 1
2551
+ """
2552
+ print("Constraints are:")
2553
+ b = self.milp.get_backend()
2554
+ varid_name = {}
2555
+ for i in range(b.ncols()):
2556
+ s = b.col_name(i)
2557
+ default_name = str(self.milp.linear_functions_parent()({i: 1}))
2558
+ if s and s != default_name:
2559
+ varid_name[i] = s
2560
+ else:
2561
+ varid_name[i] = default_name
2562
+ for i, (lb, (indices, values), ub) in enumerate(self.milp.constraints()):
2563
+ if b.row_name(i):
2564
+ print(" " + b.row_name(i) + ":", end=" ")
2565
+ if lb is not None:
2566
+ print(str(ZZ(lb)) + " <=", end=" ")
2567
+ first = True
2568
+ for j, c in sorted(zip(indices, values)):
2569
+ c = ZZ(c)
2570
+ if c == 0:
2571
+ continue
2572
+ print((("+ " if (not first and c > 0) else "") +
2573
+ ("" if c == 1 else
2574
+ ("- " if c == -1 else
2575
+ (str(c) + " " if first and c < 0 else
2576
+ ("- " + str(abs(c)) + " " if c < 0 else str(c) + " "))))
2577
+ + varid_name[j]), end=" ")
2578
+ first = False
2579
+ # Upper bound
2580
+ print("<= " + str(ZZ(ub)) if ub is not None else "")
2581
+
2582
+ if variables:
2583
+ print("Variables are:")
2584
+ P = self._bijectionist._P.root_to_elements_dict()
2585
+ for (p, z), v in self._x.items():
2586
+ print(f" {v}: " + "".join([f"s({a}) = "
2587
+ for a in P[p]]) + f"{z}")
2588
+
2589
+ def _prepare_solution(self, on_blocks, solution):
2590
+ r"""
2591
+ Return the solution as a dictionary from `A` (or `P`) to
2592
+ `Z`.
2593
+
2594
+ INPUT:
2595
+
2596
+ - ``on_blocks`` -- whether to return the solution on blocks
2597
+ or on all elements
2598
+
2599
+ TESTS::
2600
+
2601
+ sage: A = B = ["a", "b", "c"]
2602
+ sage: bij = Bijectionist(A, B, lambda x: 0)
2603
+ sage: bij.set_constant_blocks([["a", "b"]])
2604
+ sage: next(bij.solutions_iterator())
2605
+ {'a': 0, 'b': 0, 'c': 0}
2606
+ sage: bmilp = bij._bmilp
2607
+ sage: bmilp._prepare_solution(True, bmilp._solution_cache[0])
2608
+ {'a': 0, 'c': 0}
2609
+ """
2610
+ P = self._bijectionist._P
2611
+ tZ = self._bijectionist._possible_block_values
2612
+ mapping = {} # A -> Z or P -> Z, a +-> s(a)
2613
+ for p, block in P.root_to_elements_dict().items():
2614
+ for z in tZ[p]:
2615
+ if solution[p, z] == 1:
2616
+ if on_blocks:
2617
+ mapping[p] = z
2618
+ else:
2619
+ for a in block:
2620
+ mapping[a] = z
2621
+ break
2622
+ return mapping
2623
+
2624
+ def solutions_iterator(self, on_blocks, additional_constraints):
2625
+ r"""
2626
+ Iterate over all solutions satisfying the additional constraints.
2627
+
2628
+ INPUT:
2629
+
2630
+ - ``additional_constraints`` -- list of constraints for the
2631
+ underlying MILP
2632
+
2633
+ - ``on_blocks`` -- whether to return the solution on blocks or
2634
+ on all elements
2635
+
2636
+ TESTS::
2637
+
2638
+ sage: A = B = 'abc'
2639
+ sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2, solver='GLPK')
2640
+ sage: from sage.combinat.bijectionist import _BijectionistMILP
2641
+ sage: bmilp = _BijectionistMILP(bij)
2642
+ sage: it = bmilp.solutions_iterator(False, [])
2643
+ sage: it2 = bmilp.solutions_iterator(False, [bmilp._x[('c', 1)] == 1])
2644
+ sage: next(it)
2645
+ {'a': 0, 'b': 1, 'c': 0}
2646
+ sage: next(it2)
2647
+ {'a': 0, 'b': 0, 'c': 1}
2648
+ sage: next(it)
2649
+ {'a': 0, 'b': 0, 'c': 1}
2650
+ sage: next(it)
2651
+ {'a': 1, 'b': 0, 'c': 0}
2652
+ """
2653
+ i = 0 # the first unconsidered element of _solution_cache
2654
+ while True:
2655
+ # skip solutions which do not satisfy additional_constraints
2656
+ while i < len(self._solution_cache):
2657
+ solution = self._solution_cache[i]
2658
+ i += 1
2659
+ if all(self._is_solution(constraint, solution)
2660
+ for constraint in additional_constraints):
2661
+ yield self._prepare_solution(on_blocks, solution)
2662
+ break
2663
+ else:
2664
+ new_indices = []
2665
+ for constraint in additional_constraints:
2666
+ new_indices.extend(self.milp.add_constraint(constraint,
2667
+ return_indices=True))
2668
+ try:
2669
+ self.milp.solve()
2670
+ # moving this out of the try...finally block breaks SCIP
2671
+ solution = self.milp.get_values(self._x,
2672
+ convert=bool, tolerance=0.1)
2673
+ except MIPSolverException:
2674
+ return
2675
+ finally:
2676
+ b = self.milp.get_backend()
2677
+ if hasattr(b, "_get_model"):
2678
+ m = b._get_model()
2679
+ if m.getStatus() != 'unknown':
2680
+ m.freeTransform()
2681
+ self.milp.remove_constraints(new_indices)
2682
+
2683
+ self._add_solution(solution)
2684
+ i += 1
2685
+ assert i == len(self._solution_cache)
2686
+ yield self._prepare_solution(on_blocks, solution)
2687
+ if get_verbose() >= 2:
2688
+ print("after vetoing")
2689
+ self.show(variables=False)
2690
+
2691
+ def _add_solution(self, solution):
2692
+ r"""
2693
+ Add the ``solution`` to the cache and an appropriate
2694
+ veto constraint to the MILP.
2695
+
2696
+ INPUT:
2697
+
2698
+ - ``solution`` -- dictionary from the indices of the MILP to
2699
+ a boolean
2700
+
2701
+ EXAMPLES::
2702
+
2703
+ sage: A = B = ["a", "b"]
2704
+ sage: bij = Bijectionist(A, B)
2705
+ sage: from sage.combinat.bijectionist import _BijectionistMILP
2706
+ sage: bmilp = _BijectionistMILP(bij)
2707
+ sage: bmilp._add_solution({(a, b): a == b for a in A for b in B})
2708
+ sage: bmilp.show() # random
2709
+ Constraints are:
2710
+ block a: 1 <= x_0 + x_1 <= 1
2711
+ block b: 1 <= x_2 + x_3 <= 1
2712
+ statistics: 1 <= x_1 + x_3 <= 1
2713
+ statistics: 1 <= x_0 + x_2 <= 1
2714
+ veto: x_1 + x_2 <= 1
2715
+ Variables are:
2716
+ x_0: s(a) = b
2717
+ x_1: s(a) = a
2718
+ x_2: s(b) = b
2719
+ x_3: s(b) = a
2720
+ """
2721
+ active_vars = [self._x[p, z]
2722
+ for p in _disjoint_set_roots(self._bijectionist._P)
2723
+ for z in self._bijectionist._possible_block_values[p]
2724
+ if solution[(p, z)]]
2725
+ self.milp.add_constraint(sum(active_vars) <= len(active_vars) - 1,
2726
+ name='veto')
2727
+ self._solution_cache.append(solution)
2728
+
2729
+ def _is_solution(self, constraint, values):
2730
+ r"""
2731
+ Evaluate the given function at the given values.
2732
+
2733
+ INPUT:
2734
+
2735
+ - ``constraint`` -- a
2736
+ :class:`sage.numerical.linear_functions.LinearConstraint`
2737
+
2738
+ - ``values`` -- a candidate for a solution of the MILP as a
2739
+ dictionary from pairs `(a, z)\in A\times Z` to `0` or `1`,
2740
+ specifying whether `a` is mapped to `z`.
2741
+
2742
+ EXAMPLES::
2743
+
2744
+ sage: A = B = ["a", "b"]
2745
+ sage: bij = Bijectionist(A, B)
2746
+ sage: from sage.combinat.bijectionist import _BijectionistMILP
2747
+ sage: bmilp = _BijectionistMILP(bij)
2748
+ sage: f = bmilp._x["a", "a"] + bmilp._x["b", "a"] >= bmilp._x["b", "b"] + 1
2749
+ sage: v = {('a', 'a'): 1, ('a', 'b'): 0, ('b', 'a'): 1, ('b', 'b'): 1}
2750
+ sage: bmilp._is_solution(f, v)
2751
+ True
2752
+ sage: v = {('a', 'a'): 0, ('a', 'b'): 0, ('b', 'a'): 1, ('b', 'b'): 1}
2753
+ sage: bmilp._is_solution(f, v)
2754
+ False
2755
+ """
2756
+ index_block_value_dict = {}
2757
+ for (p, z), v in self._x.items():
2758
+ variable_index = next(iter(v.dict()))
2759
+ index_block_value_dict[variable_index] = (p, z)
2760
+
2761
+ def evaluate(f):
2762
+ return sum(coeff if index == -1 else
2763
+ coeff * values[index_block_value_dict[index]]
2764
+ for index, coeff in f.dict().items())
2765
+
2766
+ for lhs, rhs in constraint.equations():
2767
+ if evaluate(lhs - rhs):
2768
+ return False
2769
+ for lhs, rhs in constraint.inequalities():
2770
+ if evaluate(lhs - rhs) > 0:
2771
+ return False
2772
+ return True
2773
+
2774
+ def add_alpha_beta_constraints(self):
2775
+ r"""
2776
+ Add constraints enforcing that `(alpha, s)` is equidistributed
2777
+ with `(beta, tau)` and `S` is the intertwining bijection.
2778
+
2779
+ We do this by adding
2780
+
2781
+ .. MATH::
2782
+
2783
+ \sum_{a\in A, z\in Z} x_{p(a), z} s^z t^{\alpha(a)}
2784
+ = \sum_{b\in B} s^{\tau(b)} t(\beta(b))
2785
+
2786
+ as a matrix equation.
2787
+
2788
+ EXAMPLES::
2789
+
2790
+ sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)]
2791
+ sage: bij = Bijectionist(A, B, len)
2792
+ sage: bij.set_statistics((len, len))
2793
+ sage: from sage.combinat.bijectionist import _BijectionistMILP
2794
+ sage: bmilp = _BijectionistMILP(bij) # indirect doctest
2795
+ sage: next(bmilp.solutions_iterator(False, []))
2796
+ {[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 2}
2797
+ """
2798
+ W = self._bijectionist._W
2799
+ Z = self._bijectionist._Z
2800
+ zero = self.milp.linear_functions_parent().zero()
2801
+ AZ_matrix = [[zero] * len(W) for _ in range(len(Z))]
2802
+ B_matrix = [[zero] * len(W) for _ in range(len(Z))]
2803
+
2804
+ W_dict = {w: i for i, w in enumerate(W)}
2805
+ Z_dict = {z: i for i, z in enumerate(Z)}
2806
+
2807
+ for a in self._bijectionist._A:
2808
+ p = self._bijectionist._P.find(a)
2809
+ for z in self._bijectionist._possible_block_values[p]:
2810
+ w_index = W_dict[self._bijectionist._alpha(a)]
2811
+ z_index = Z_dict[z]
2812
+ AZ_matrix[z_index][w_index] += self._x[p, z]
2813
+
2814
+ for b in self._bijectionist._B:
2815
+ w_index = W_dict[self._bijectionist._beta(b)]
2816
+ z_index = Z_dict[self._bijectionist._tau[b]]
2817
+ B_matrix[z_index][w_index] += 1
2818
+
2819
+ for w in range(len(W)):
2820
+ for z in range(len(Z)):
2821
+ self.milp.add_constraint(AZ_matrix[z][w] == B_matrix[z][w],
2822
+ name='statistics')
2823
+
2824
+ def add_distribution_constraints(self):
2825
+ r"""
2826
+ Add constraints so the distributions given by
2827
+ :meth:`set_distributions` are fulfilled.
2828
+
2829
+ To accomplish this we add
2830
+
2831
+ .. MATH::
2832
+
2833
+ \sum_{a\in\tilde A} x_{p(a), z}t^z = \sum_{z\in\tilde Z} t^z,
2834
+
2835
+ where `p(a)` is the block containing `a`, for each given
2836
+ distribution as a vector equation.
2837
+
2838
+ EXAMPLES::
2839
+
2840
+ sage: A = B = Permutations(3)
2841
+ sage: tau = Permutation.longest_increasing_subsequence_length
2842
+ sage: bij = Bijectionist(A, B, tau)
2843
+ sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [1, 3]))
2844
+ sage: from sage.combinat.bijectionist import _BijectionistMILP
2845
+ sage: bmilp = _BijectionistMILP(bij) # indirect doctest
2846
+ sage: next(bmilp.solutions_iterator(False, []))
2847
+ {[1, 2, 3]: 3,
2848
+ [1, 3, 2]: 1,
2849
+ [2, 1, 3]: 2,
2850
+ [2, 3, 1]: 2,
2851
+ [3, 1, 2]: 2,
2852
+ [3, 2, 1]: 2}
2853
+ """
2854
+ Z = self._bijectionist._Z
2855
+ Z_dict = {z: i for i, z in enumerate(Z)}
2856
+ zero = self.milp.linear_functions_parent().zero()
2857
+ for tA, tZ in self._bijectionist._elements_distributions:
2858
+ tA_sum = [zero] * len(Z_dict)
2859
+ tZ_sum = [zero] * len(Z_dict)
2860
+ for a in tA:
2861
+ p = self._bijectionist._P.find(a)
2862
+ for z in self._bijectionist._possible_block_values[p]:
2863
+ tA_sum[Z_dict[z]] += self._x[p, z]
2864
+ for z in tZ:
2865
+ tZ_sum[Z_dict[z]] += 1
2866
+
2867
+ for a, z in zip(tA_sum, tZ_sum):
2868
+ self.milp.add_constraint(a == z, name=f"d: {a} == {z}")
2869
+
2870
+ def add_intertwining_relation_constraints(self):
2871
+ r"""
2872
+ Add constraints corresponding to the given intertwining
2873
+ relations.
2874
+
2875
+ INPUT:
2876
+
2877
+ - origins, a list of triples `((\pi/\rho, p,
2878
+ (p_1,\dots,p_k))`, where `p` is the block of
2879
+ `\rho(s(a_1),\dots, s(a_k))`, for any `a_i\in p_i`.
2880
+
2881
+ This adds the constraints imposed by
2882
+ :meth:`set_intertwining_relations`,
2883
+
2884
+ .. MATH::
2885
+
2886
+ s(\pi(a_1,\dots, a_k)) = \rho(s(a_1),\dots, s(a_k))
2887
+
2888
+ for each pair `(\pi, \rho)`. The relation implies
2889
+ immediately that `s(\pi(a_1,\dots, a_k))` only depends on the
2890
+ blocks of `a_1,\dots, a_k`.
2891
+
2892
+ The MILP formulation is as follows. Let `a_1,\dots,a_k \in
2893
+ A` and let `a = \pi(a_1,\dots,a_k)`. Let `z_1,\dots,z_k \in
2894
+ Z` and let `z = \rho(z_1,\dots,z_k)`. Suppose that `a_i\in
2895
+ p_i` for all `i` and that `a\in p`.
2896
+
2897
+ We then want to model the implication
2898
+
2899
+ .. MATH::
2900
+
2901
+ x_{p_1, z_1} = 1,\dots, x_{p_k, z_k} = 1 \Rightarrow x_{p, z} = 1.
2902
+
2903
+ We achieve this by requiring
2904
+
2905
+ .. MATH::
2906
+
2907
+ x_{p, z}\geq 1 - k + \sum_{i=1}^k x_{p_i, z_i}.
2908
+
2909
+ Note that `z` must be a possible value of `p` and each `z_i`
2910
+ must be a possible value of `p_i`.
2911
+
2912
+ EXAMPLES::
2913
+
2914
+ sage: A = B = 'abcd'
2915
+ sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2)
2916
+ sage: def pi(p1, p2): return 'abcdefgh'[A.index(p1) + A.index(p2)]
2917
+ sage: def rho(s1, s2): return (s1 + s2) % 2
2918
+ sage: bij.set_intertwining_relations((2, pi, rho))
2919
+ sage: from sage.combinat.bijectionist import _BijectionistMILP
2920
+ sage: bmilp = _BijectionistMILP(bij) # indirect doctest
2921
+ sage: next(bmilp.solutions_iterator(False, []))
2922
+ {'a': 0, 'b': 1, 'c': 0, 'd': 1}
2923
+ """
2924
+ A = self._bijectionist._A
2925
+ tZ = self._bijectionist._possible_block_values
2926
+ P = self._bijectionist._P
2927
+ for composition_index, pi_rho in enumerate(self._bijectionist._pi_rho):
2928
+ pi_blocks = set()
2929
+ for a_tuple in itertools.product(A, repeat=pi_rho.numargs):
2930
+ if pi_rho.domain is not None and not pi_rho.domain(*a_tuple):
2931
+ continue
2932
+ a = pi_rho.pi(*a_tuple)
2933
+ if a in A:
2934
+ p_tuple = tuple(P.find(a) for a in a_tuple)
2935
+ p = P.find(a)
2936
+ if (p_tuple, p) not in pi_blocks:
2937
+ pi_blocks.add((p_tuple, p))
2938
+ for z_tuple in itertools.product(*[tZ[p] for p in p_tuple]):
2939
+ rhs = (1 - pi_rho.numargs
2940
+ + sum(self._x[p_i, z_i]
2941
+ for p_i, z_i in zip(p_tuple, z_tuple)))
2942
+ z = pi_rho.rho(*z_tuple)
2943
+ if z in tZ[p]:
2944
+ c = self._x[p, z] - rhs
2945
+ if c.is_zero():
2946
+ continue
2947
+ self.milp.add_constraint(c >= 0,
2948
+ name=f"pi/rho({composition_index})")
2949
+ else:
2950
+ self.milp.add_constraint(rhs <= 0,
2951
+ name=f"pi/rho({composition_index})")
2952
+
2953
+ def add_quadratic_relation_constraints(self):
2954
+ r"""
2955
+ Add constraints enforcing that `s\circ\phi\circ s =
2956
+ \psi`.
2957
+
2958
+ We do this by adding
2959
+
2960
+ .. MATH::
2961
+
2962
+ x_{p(a), z} = x_{p(\psi(z)), \phi(a)}
2963
+
2964
+ for `a\in A` and `z\in Z`, where `\phi:A\to Z` and `\psi:Z\to
2965
+ A`. Note that, in particular, `\phi` must be constant on
2966
+ blocks.
2967
+
2968
+ EXAMPLES::
2969
+
2970
+ sage: A = B = DyckWords(3)
2971
+ sage: bij = Bijectionist(A, B)
2972
+ sage: bij.set_statistics((lambda D: D.number_of_touch_points(), lambda D: D.number_of_initial_rises()))
2973
+ sage: ascii_art(sorted(bij.minimal_subdistributions_iterator()))
2974
+ [ ( [ /\ ] )
2975
+ [ ( [ / \ ] ) ( [ /\ /\ ] [ /\ /\/\ ] )
2976
+ [ ( [ /\/\/\ ], [ / \ ] ), ( [ /\/ \, / \/\ ], [ / \/\, / \ ] ),
2977
+ <BLANKLINE>
2978
+ ( [ /\ ] ) ]
2979
+ ( [ /\/\ / \ ] [ /\ ] ) ]
2980
+ ( [ / \, / \ ], [ /\/\/\, /\/ \ ] ) ]
2981
+ sage: bij.set_quadratic_relation((lambda D: D, lambda D: D)) # indirect doctest
2982
+ sage: ascii_art(sorted(bij.minimal_subdistributions_iterator()))
2983
+ [ ( [ /\ ] )
2984
+ [ ( [ / \ ] ) ( [ /\ ] [ /\/\ ] )
2985
+ [ ( [ /\/\/\ ], [ / \ ] ), ( [ /\/ \ ], [ / \ ] ),
2986
+ <BLANKLINE>
2987
+ <BLANKLINE>
2988
+ ( [ /\ ] [ /\ ] ) ( [ /\/\ ] [ /\ ] )
2989
+ ( [ / \/\ ], [ / \/\ ] ), ( [ / \ ], [ /\/ \ ] ),
2990
+ <BLANKLINE>
2991
+ ( [ /\ ] ) ]
2992
+ ( [ / \ ] ) ]
2993
+ ( [ / \ ], [ /\/\/\ ] ) ]
2994
+ """
2995
+ P = self._bijectionist._P
2996
+ for phi, psi in self._bijectionist._phi_psi:
2997
+ for p, block in P.root_to_elements_dict().items():
2998
+ z0 = phi(p)
2999
+ assert all(phi(a) == z0 for a in block), "phi must be constant on the block %s" % block
3000
+ for z in self._bijectionist._possible_block_values[p]:
3001
+ p0 = P.find(psi(z))
3002
+ if z0 in self._bijectionist._possible_block_values[p0]:
3003
+ c = self._x[p, z] - self._x[p0, z0]
3004
+ if c.is_zero():
3005
+ continue
3006
+ self.milp.add_constraint(c == 0, name=f"i: s({p})={z}<->s(psi({z})=phi({p})")
3007
+ else:
3008
+ self.milp.add_constraint(self._x[p, z] == 0, name=f"i: s({p})!={z}")
3009
+
3010
+ def add_homomesic_constraints(self):
3011
+ r"""
3012
+ Add constraints enforcing that `s` has constant average
3013
+ on the blocks of `Q`.
3014
+
3015
+ We do this by adding
3016
+
3017
+ .. MATH::
3018
+
3019
+ \frac{1}{|q|}\sum_{a\in q} \sum_z z x_{p(a), z} =
3020
+ \frac{1}{|q_0|}\sum_{a\in q_0} \sum_z z x_{p(a), z},
3021
+
3022
+ for `q\in Q`, where `q_0` is some fixed block of `Q`.
3023
+
3024
+ EXAMPLES::
3025
+
3026
+ sage: A = B = [1,2,3]
3027
+ sage: bij = Bijectionist(A, B, lambda b: b % 3)
3028
+ sage: bij.set_homomesic([[1,2], [3]]) # indirect doctest
3029
+ sage: list(bij.solutions_iterator())
3030
+ [{1: 2, 2: 0, 3: 1}, {1: 0, 2: 2, 3: 1}]
3031
+ """
3032
+ Q = self._bijectionist._Q
3033
+ if Q is None:
3034
+ return
3035
+ P = self._bijectionist._P
3036
+ tZ = self._bijectionist._possible_block_values
3037
+
3038
+ def sum_q(q):
3039
+ return sum(sum(z * self._x[P.find(a), z] for z in tZ[P.find(a)])
3040
+ for a in q)
3041
+ q0 = Q[0]
3042
+ v0 = sum_q(q0)
3043
+ for q in Q[1:]:
3044
+ self.milp.add_constraint(len(q0) * sum_q(q) == len(q) * v0,
3045
+ name=f"h: ({q})~({q0})")
3046
+
3047
+
3048
+ def _invert_dict(d):
3049
+ """
3050
+ Return the dictionary whose keys are the values of the input and
3051
+ whose values are the lists of preimages.
3052
+
3053
+ INPUT:
3054
+
3055
+ - ``d`` -- dictionary
3056
+
3057
+ EXAMPLES::
3058
+
3059
+ sage: from sage.combinat.bijectionist import _invert_dict
3060
+ sage: _invert_dict({1: "a", 2: "a", 3:"b"})
3061
+ {'a': [1, 2], 'b': [3]}
3062
+
3063
+ sage: _invert_dict({})
3064
+ {}
3065
+ """
3066
+ preimages = {}
3067
+ for k, v in d.items():
3068
+ preimages[v] = preimages.get(v, []) + [k]
3069
+ return preimages
3070
+
3071
+
3072
+ def _disjoint_set_roots(d):
3073
+ """
3074
+ Return the representatives of the blocks of the disjoint set.
3075
+
3076
+ INPUT:
3077
+
3078
+ - ``d`` -- a :class:`sage.sets.disjoint_set.DisjointSet_of_hashables`
3079
+
3080
+ EXAMPLES::
3081
+
3082
+ sage: from sage.combinat.bijectionist import _disjoint_set_roots
3083
+ sage: d = DisjointSet('abcde')
3084
+ sage: d.union("a", "b")
3085
+ sage: d.union("a", "c")
3086
+ sage: d.union("e", "d")
3087
+ sage: _disjoint_set_roots(d)
3088
+ dict_keys(['a', 'e'])
3089
+ """
3090
+ return d.root_to_elements_dict().keys()
3091
+
3092
+
3093
+ def _non_copying_intersection(sets):
3094
+ """
3095
+ Return the intersection of the sets.
3096
+
3097
+ If the intersection is equal to one of the sets, return this
3098
+ set.
3099
+
3100
+ EXAMPLES::
3101
+
3102
+ sage: from sage.combinat.bijectionist import _non_copying_intersection
3103
+ sage: A = set(range(7000)); B = set(range(8000));
3104
+ sage: _non_copying_intersection([A, B]) is A
3105
+ True
3106
+
3107
+ sage: A = set([1,2]); B = set([2,3])
3108
+ sage: _non_copying_intersection([A, B])
3109
+ {2}
3110
+ """
3111
+ sets = sorted(sets, key=len)
3112
+ result = set.intersection(*sets)
3113
+ n = len(result)
3114
+ for s in sets:
3115
+ N = len(s)
3116
+ if n < N:
3117
+ return result
3118
+ if s == result:
3119
+ return s
3120
+
3121
+
3122
+ """
3123
+ TESTS::
3124
+
3125
+ sage: As = Bs = [[],
3126
+ ....: [(1,i,j) for i in [-1,0,1] for j in [-1,1]],
3127
+ ....: [(2,i,j) for i in [-1,0,1] for j in [-1,1]],
3128
+ ....: [(3,i,j) for i in [-2,-1,0,1,2] for j in [-1,1]]]
3129
+
3130
+ Note that adding ``[(2,-2,-1), (2,2,-1), (2,-2,1), (2,2,1)]`` makes
3131
+ it take (seemingly) forever::
3132
+
3133
+ sage: def c1(a, b): return (a[0]+b[0], a[1]*b[1], a[2]*b[2])
3134
+ sage: def c2(a): return (a[0], -a[1], a[2])
3135
+
3136
+ sage: bij = Bijectionist(sum(As, []), sum(Bs, []))
3137
+ sage: bij.set_statistics((lambda x: x[0], lambda x: x[0]))
3138
+ sage: bij.set_intertwining_relations((2, c1, c1), (1, c2, c2))
3139
+ 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)
3140
+ 64
3141
+
3142
+ A brute force check would be difficult::
3143
+
3144
+ sage: prod([factorial(len(A)) for A in As])
3145
+ 1881169920000
3146
+
3147
+ Let us try a smaller example::
3148
+
3149
+ sage: As = Bs = [[],
3150
+ ....: [(1,i,j) for i in [-1,0,1] for j in [-1,1]],
3151
+ ....: [(2,i,j) for i in [-1,1] for j in [-1,1]],
3152
+ ....: [(3,i,j) for i in [-1,1] for j in [-1,1]]]
3153
+
3154
+ sage: bij = Bijectionist(sum(As, []), sum(Bs, []))
3155
+ sage: bij.set_statistics((lambda x: x[0], lambda x: x[0]))
3156
+ sage: bij.set_intertwining_relations((2, c1, c1), (1, c2, c2))
3157
+ sage: l1 = list(bij.solutions_iterator()); len(l1)
3158
+ 16
3159
+ sage: prod([factorial(len(A)) for A in As])
3160
+ 414720
3161
+
3162
+ sage: pis = cartesian_product([Permutations(len(A)) for A in As])
3163
+ 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)
3164
+ sage: A = sum(As, [])
3165
+ 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)
3166
+ sage: respects_c2 = lambda s: all(c2(a1) not in A or s[c2(a1)] == c2(s[a1]) for a1 in A)
3167
+ 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)
3168
+ sage: sorted(l1, key=lambda s: tuple(s.items())) == l2 # long time
3169
+ True
3170
+
3171
+ Our benchmark example::
3172
+
3173
+ sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition
3174
+ sage: def alpha1(p): return len(p.weak_excedences())
3175
+ sage: def alpha2(p): return len(p.fixed_points())
3176
+ sage: def beta1(p): return len(p.descents(final_descent=True)) if p else 0
3177
+ sage: def beta2(p): return len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1])
3178
+ sage: gamma = Permutation.longest_increasing_subsequence_length
3179
+ sage: def rotate_permutation(p):
3180
+ ....: cycle = Permutation(tuple(range(1, len(p)+1)))
3181
+ ....: return Permutation([cycle.inverse()(p(cycle(i))) for i in range(1, len(p)+1)])
3182
+
3183
+ sage: N = 5
3184
+ sage: As = [list(Permutations(n)) for n in range(N+1)]
3185
+ sage: A = B = sum(As, [])
3186
+ sage: bij = Bijectionist(A, B, gamma)
3187
+ sage: bij.set_statistics((len, len), (alpha1, beta1), (alpha2, beta2))
3188
+ sage: bij.set_constant_blocks(sum([orbit_decomposition(A, rotate_permutation) for A in As], []))
3189
+
3190
+ sage: P = bij.constant_blocks(optimal=True)
3191
+ sage: P = [sorted(p, key=lambda p: (len(p), p)) for p in P]
3192
+ sage: P = sorted(P, key=lambda p: (len(next(iter(p))), len(p)))
3193
+ sage: for p in P:
3194
+ ....: print(p)
3195
+ [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5]]
3196
+ [[2, 1], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], ...
3197
+ [[3, 1, 2], [1, 4, 2, 3], [2, 4, 1, 3], [3, 1, 2, 4], [3, 1, 4, 2], ...
3198
+ [[4, 1, 2, 3], [1, 5, 2, 3, 4], [4, 1, 2, 3, 5], [4, 5, 1, 2, 3], [5, 1, 2, 4, 3], ...
3199
+ [[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]]
3200
+ [[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]]
3201
+ ...
3202
+
3203
+ sage: for d in sorted(bij.minimal_subdistributions_blocks_iterator(), key=lambda d: (len(d[0]), d[0])):
3204
+ ....: print(d)
3205
+ ([[]], [0])
3206
+ ([[1]], [1])
3207
+ ([[2, 1]], [2])
3208
+ ([[3, 1, 2]], [3])
3209
+ ([[4, 1, 2, 3]], [4])
3210
+ ([[5, 1, 2, 3, 4]], [5])
3211
+ ([[2, 3, 1, 5, 4], [2, 4, 5, 3, 1], [2, 5, 4, 1, 3], [3, 4, 1, 5, 2]], [2, 3, 3, 3])
3212
+ ([[3, 1, 2, 5, 4], [4, 1, 2, 5, 3], [3, 5, 2, 1, 4], [4, 1, 5, 2, 3]], [3, 3, 4, 4])
3213
+ ([[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])
3214
+
3215
+ 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)
3216
+ 504
3217
+
3218
+ sage: for a, d in bij.minimal_subdistributions_iterator(): # not tested
3219
+ ....: print(sorted(a), sorted(d))
3220
+ """