passagemath-graphs 10.6.1rc1__cp310-cp310-musllinux_1_2_aarch64.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.
- passagemath_graphs-10.6.1rc1.dist-info/METADATA +292 -0
- passagemath_graphs-10.6.1rc1.dist-info/RECORD +260 -0
- passagemath_graphs-10.6.1rc1.dist-info/WHEEL +5 -0
- passagemath_graphs-10.6.1rc1.dist-info/top_level.txt +2 -0
- passagemath_graphs.libs/libgcc_s-69c45f16.so.1 +0 -0
- passagemath_graphs.libs/libgmp-8e78bd9b.so.10.5.0 +0 -0
- passagemath_graphs.libs/libstdc++-1f1a71be.so.6.0.33 +0 -0
- sage/all__sagemath_graphs.py +39 -0
- sage/combinat/abstract_tree.py +2723 -0
- sage/combinat/all__sagemath_graphs.py +34 -0
- sage/combinat/binary_tree.py +5306 -0
- sage/combinat/cluster_algebra_quiver/all.py +22 -0
- sage/combinat/cluster_algebra_quiver/cluster_seed.py +5208 -0
- sage/combinat/cluster_algebra_quiver/interact.py +124 -0
- sage/combinat/cluster_algebra_quiver/mutation_class.py +625 -0
- sage/combinat/cluster_algebra_quiver/mutation_type.py +1555 -0
- sage/combinat/cluster_algebra_quiver/quiver.py +2290 -0
- sage/combinat/cluster_algebra_quiver/quiver_mutation_type.py +2468 -0
- sage/combinat/designs/MOLS_handbook_data.py +570 -0
- sage/combinat/designs/all.py +58 -0
- sage/combinat/designs/bibd.py +1655 -0
- sage/combinat/designs/block_design.py +1071 -0
- sage/combinat/designs/covering_array.py +269 -0
- sage/combinat/designs/covering_design.py +530 -0
- sage/combinat/designs/database.py +5615 -0
- sage/combinat/designs/design_catalog.py +122 -0
- sage/combinat/designs/designs_pyx.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/designs/designs_pyx.pxd +21 -0
- sage/combinat/designs/designs_pyx.pyx +993 -0
- sage/combinat/designs/difference_family.py +3951 -0
- sage/combinat/designs/difference_matrices.py +279 -0
- sage/combinat/designs/evenly_distributed_sets.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/designs/evenly_distributed_sets.pyx +661 -0
- sage/combinat/designs/ext_rep.py +1064 -0
- sage/combinat/designs/gen_quadrangles_with_spread.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/designs/gen_quadrangles_with_spread.pyx +339 -0
- sage/combinat/designs/group_divisible_designs.py +361 -0
- sage/combinat/designs/incidence_structures.py +2357 -0
- sage/combinat/designs/latin_squares.py +581 -0
- sage/combinat/designs/orthogonal_arrays.py +2244 -0
- sage/combinat/designs/orthogonal_arrays_build_recursive.py +1780 -0
- sage/combinat/designs/orthogonal_arrays_find_recursive.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/designs/orthogonal_arrays_find_recursive.pyx +967 -0
- sage/combinat/designs/resolvable_bibd.py +815 -0
- sage/combinat/designs/steiner_quadruple_systems.py +1306 -0
- sage/combinat/designs/subhypergraph_search.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/designs/subhypergraph_search.pyx +530 -0
- sage/combinat/designs/twographs.py +306 -0
- sage/combinat/finite_state_machine.py +14874 -0
- sage/combinat/finite_state_machine_generators.py +2006 -0
- sage/combinat/graph_path.py +448 -0
- sage/combinat/interval_posets.py +3908 -0
- sage/combinat/nu_tamari_lattice.py +269 -0
- sage/combinat/ordered_tree.py +1446 -0
- sage/combinat/posets/all.py +46 -0
- sage/combinat/posets/bubble_shuffle.py +247 -0
- sage/combinat/posets/cartesian_product.py +493 -0
- sage/combinat/posets/d_complete.py +182 -0
- sage/combinat/posets/elements.py +273 -0
- sage/combinat/posets/forest.py +30 -0
- sage/combinat/posets/hasse_cython.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/posets/hasse_cython.pyx +174 -0
- sage/combinat/posets/hasse_diagram.py +3672 -0
- sage/combinat/posets/hochschild_lattice.py +158 -0
- sage/combinat/posets/incidence_algebras.py +794 -0
- sage/combinat/posets/lattices.py +5117 -0
- sage/combinat/posets/linear_extension_iterator.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/posets/linear_extension_iterator.pyx +292 -0
- sage/combinat/posets/linear_extensions.py +1037 -0
- sage/combinat/posets/mobile.py +275 -0
- sage/combinat/posets/moebius_algebra.py +776 -0
- sage/combinat/posets/poset_examples.py +2178 -0
- sage/combinat/posets/posets.py +9360 -0
- sage/combinat/rooted_tree.py +1070 -0
- sage/combinat/shard_order.py +239 -0
- sage/combinat/tamari_lattices.py +384 -0
- sage/combinat/yang_baxter_graph.py +923 -0
- sage/databases/all__sagemath_graphs.py +1 -0
- sage/databases/knotinfo_db.py +1231 -0
- sage/ext_data/all__sagemath_graphs.py +1 -0
- sage/ext_data/graphs/graph_plot_js.html +330 -0
- sage/ext_data/kenzo/CP2.txt +45 -0
- sage/ext_data/kenzo/CP3.txt +349 -0
- sage/ext_data/kenzo/CP4.txt +4774 -0
- sage/ext_data/kenzo/README.txt +49 -0
- sage/ext_data/kenzo/S4.txt +20 -0
- sage/graphs/all.py +42 -0
- sage/graphs/asteroidal_triples.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/asteroidal_triples.pyx +320 -0
- sage/graphs/base/all.py +1 -0
- sage/graphs/base/boost_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/boost_graph.pxd +106 -0
- sage/graphs/base/boost_graph.pyx +3045 -0
- sage/graphs/base/c_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/c_graph.pxd +106 -0
- sage/graphs/base/c_graph.pyx +5096 -0
- sage/graphs/base/dense_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/dense_graph.pxd +28 -0
- sage/graphs/base/dense_graph.pyx +801 -0
- sage/graphs/base/graph_backends.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/graph_backends.pxd +5 -0
- sage/graphs/base/graph_backends.pyx +797 -0
- sage/graphs/base/overview.py +85 -0
- sage/graphs/base/sparse_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/sparse_graph.pxd +90 -0
- sage/graphs/base/sparse_graph.pyx +1653 -0
- sage/graphs/base/static_dense_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/static_dense_graph.pxd +5 -0
- sage/graphs/base/static_dense_graph.pyx +1032 -0
- sage/graphs/base/static_sparse_backend.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/static_sparse_backend.pxd +27 -0
- sage/graphs/base/static_sparse_backend.pyx +1583 -0
- sage/graphs/base/static_sparse_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/static_sparse_graph.pxd +37 -0
- sage/graphs/base/static_sparse_graph.pyx +1375 -0
- sage/graphs/bipartite_graph.py +2732 -0
- sage/graphs/centrality.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/centrality.pyx +1038 -0
- sage/graphs/cographs.py +519 -0
- sage/graphs/comparability.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/comparability.pyx +851 -0
- sage/graphs/connectivity.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/connectivity.pxd +157 -0
- sage/graphs/connectivity.pyx +4813 -0
- sage/graphs/convexity_properties.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/convexity_properties.pxd +16 -0
- sage/graphs/convexity_properties.pyx +870 -0
- sage/graphs/digraph.py +4754 -0
- sage/graphs/digraph_generators.py +1993 -0
- sage/graphs/distances_all_pairs.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/distances_all_pairs.pxd +12 -0
- sage/graphs/distances_all_pairs.pyx +2938 -0
- sage/graphs/domination.py +1363 -0
- sage/graphs/dot2tex_utils.py +100 -0
- sage/graphs/edge_connectivity.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/edge_connectivity.pyx +1215 -0
- sage/graphs/generators/all.py +1 -0
- sage/graphs/generators/basic.py +1769 -0
- sage/graphs/generators/chessboard.py +538 -0
- sage/graphs/generators/classical_geometries.py +1611 -0
- sage/graphs/generators/degree_sequence.py +235 -0
- sage/graphs/generators/distance_regular.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/generators/distance_regular.pyx +2846 -0
- sage/graphs/generators/families.py +4759 -0
- sage/graphs/generators/intersection.py +565 -0
- sage/graphs/generators/platonic_solids.py +262 -0
- sage/graphs/generators/random.py +2623 -0
- sage/graphs/generators/smallgraphs.py +5741 -0
- sage/graphs/generators/world_map.py +724 -0
- sage/graphs/generic_graph.py +26867 -0
- sage/graphs/generic_graph_pyx.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/generic_graph_pyx.pxd +34 -0
- sage/graphs/generic_graph_pyx.pyx +1673 -0
- sage/graphs/genus.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/genus.pyx +622 -0
- sage/graphs/graph.py +9645 -0
- sage/graphs/graph_coloring.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_coloring.pyx +2284 -0
- sage/graphs/graph_database.py +1177 -0
- sage/graphs/graph_decompositions/all.py +1 -0
- sage/graphs/graph_decompositions/bandwidth.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/bandwidth.pyx +428 -0
- sage/graphs/graph_decompositions/clique_separators.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/clique_separators.pyx +616 -0
- sage/graphs/graph_decompositions/cutwidth.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/cutwidth.pyx +753 -0
- sage/graphs/graph_decompositions/fast_digraph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/fast_digraph.pxd +13 -0
- sage/graphs/graph_decompositions/fast_digraph.pyx +212 -0
- sage/graphs/graph_decompositions/graph_products.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/graph_products.pyx +508 -0
- sage/graphs/graph_decompositions/modular_decomposition.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/modular_decomposition.pxd +27 -0
- sage/graphs/graph_decompositions/modular_decomposition.pyx +1536 -0
- sage/graphs/graph_decompositions/slice_decomposition.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/slice_decomposition.pxd +18 -0
- sage/graphs/graph_decompositions/slice_decomposition.pyx +1106 -0
- sage/graphs/graph_decompositions/tree_decomposition.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/tree_decomposition.pxd +17 -0
- sage/graphs/graph_decompositions/tree_decomposition.pyx +1996 -0
- sage/graphs/graph_decompositions/vertex_separation.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/vertex_separation.pxd +5 -0
- sage/graphs/graph_decompositions/vertex_separation.pyx +1963 -0
- sage/graphs/graph_editor.py +82 -0
- sage/graphs/graph_generators.py +3314 -0
- sage/graphs/graph_generators_pyx.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_generators_pyx.pyx +95 -0
- sage/graphs/graph_input.py +812 -0
- sage/graphs/graph_latex.py +2064 -0
- sage/graphs/graph_list.py +410 -0
- sage/graphs/graph_plot.py +1756 -0
- sage/graphs/graph_plot_js.py +338 -0
- sage/graphs/hyperbolicity.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/hyperbolicity.pyx +1704 -0
- sage/graphs/hypergraph_generators.py +364 -0
- sage/graphs/independent_sets.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/independent_sets.pxd +13 -0
- sage/graphs/independent_sets.pyx +402 -0
- sage/graphs/isgci.py +1033 -0
- sage/graphs/isoperimetric_inequalities.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/isoperimetric_inequalities.pyx +489 -0
- sage/graphs/line_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/line_graph.pyx +743 -0
- sage/graphs/lovasz_theta.py +77 -0
- sage/graphs/matching.py +1633 -0
- sage/graphs/matching_covered_graph.py +3590 -0
- sage/graphs/orientations.py +1489 -0
- sage/graphs/partial_cube.py +459 -0
- sage/graphs/path_enumeration.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/path_enumeration.pyx +2040 -0
- sage/graphs/pq_trees.py +1129 -0
- sage/graphs/print_graphs.py +201 -0
- sage/graphs/schnyder.py +865 -0
- sage/graphs/spanning_tree.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/spanning_tree.pyx +1457 -0
- sage/graphs/strongly_regular_db.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/strongly_regular_db.pyx +3340 -0
- sage/graphs/traversals.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/traversals.pxd +9 -0
- sage/graphs/traversals.pyx +1872 -0
- sage/graphs/trees.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/trees.pxd +15 -0
- sage/graphs/trees.pyx +310 -0
- sage/graphs/tutte_polynomial.py +713 -0
- sage/graphs/views.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/views.pyx +794 -0
- sage/graphs/weakly_chordal.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/weakly_chordal.pyx +604 -0
- sage/groups/all__sagemath_graphs.py +1 -0
- sage/groups/perm_gps/all__sagemath_graphs.py +1 -0
- sage/groups/perm_gps/partn_ref/all__sagemath_graphs.py +1 -0
- sage/groups/perm_gps/partn_ref/refinement_graphs.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/groups/perm_gps/partn_ref/refinement_graphs.pxd +38 -0
- sage/groups/perm_gps/partn_ref/refinement_graphs.pyx +1666 -0
- sage/knots/all.py +6 -0
- sage/knots/free_knotinfo_monoid.py +507 -0
- sage/knots/gauss_code.py +291 -0
- sage/knots/knot.py +682 -0
- sage/knots/knot_table.py +284 -0
- sage/knots/knotinfo.py +2900 -0
- sage/knots/link.py +4715 -0
- sage/sandpiles/all.py +13 -0
- sage/sandpiles/examples.py +225 -0
- sage/sandpiles/sandpile.py +6365 -0
- sage/topology/all.py +22 -0
- sage/topology/cell_complex.py +1214 -0
- sage/topology/cubical_complex.py +1976 -0
- sage/topology/delta_complex.py +1806 -0
- sage/topology/filtered_simplicial_complex.py +744 -0
- sage/topology/moment_angle_complex.py +823 -0
- sage/topology/simplicial_complex.py +5160 -0
- sage/topology/simplicial_complex_catalog.py +92 -0
- sage/topology/simplicial_complex_examples.py +1680 -0
- sage/topology/simplicial_complex_homset.py +205 -0
- sage/topology/simplicial_complex_morphism.py +836 -0
- sage/topology/simplicial_set.py +4102 -0
- sage/topology/simplicial_set_catalog.py +55 -0
- sage/topology/simplicial_set_constructions.py +2954 -0
- sage/topology/simplicial_set_examples.py +865 -0
- sage/topology/simplicial_set_morphism.py +1464 -0
@@ -0,0 +1,2357 @@
|
|
1
|
+
# sage_setup: distribution = sagemath-graphs
|
2
|
+
r"""
|
3
|
+
Incidence structures (i.e. hypergraphs, i.e. set systems)
|
4
|
+
|
5
|
+
An incidence structure is specified by a list of points, blocks, or an incidence
|
6
|
+
matrix ([1]_, [2]_). :class:`IncidenceStructure` instances have the following methods:
|
7
|
+
|
8
|
+
{METHODS_OF_IncidenceStructure}
|
9
|
+
|
10
|
+
REFERENCES:
|
11
|
+
|
12
|
+
.. [1] Block designs and incidence structures from wikipedia,
|
13
|
+
:wikipedia:`Block_design`
|
14
|
+
:wikipedia:`Incidence_structure`
|
15
|
+
|
16
|
+
.. [2] \E. Assmus, J. Key, Designs and their codes, CUP, 1992.
|
17
|
+
|
18
|
+
AUTHORS:
|
19
|
+
|
20
|
+
- Peter Dobcsanyi and David Joyner (2007-2008)
|
21
|
+
|
22
|
+
This is a significantly modified form of part of the module block_design.py
|
23
|
+
(version 0.6) written by Peter Dobcsanyi peter@designtheory.org.
|
24
|
+
|
25
|
+
- Vincent Delecroix (2014): major rewrite
|
26
|
+
|
27
|
+
Methods
|
28
|
+
-------
|
29
|
+
"""
|
30
|
+
# **************************************************************************
|
31
|
+
# Copyright (C) 2007 #
|
32
|
+
# #
|
33
|
+
# Peter Dobcsanyi and David Joyner #
|
34
|
+
# <peter@designtheory.org> <wdjoyner@gmail.com> #
|
35
|
+
# #
|
36
|
+
# #
|
37
|
+
# Distributed under the terms of the GNU General Public License (GPL) #
|
38
|
+
# as published by the Free Software Foundation; either version 2 of #
|
39
|
+
# the License, or (at your option) any later version. #
|
40
|
+
# https://www.gnu.org/licenses/ #
|
41
|
+
# **************************************************************************
|
42
|
+
from __future__ import annotations
|
43
|
+
|
44
|
+
from sage.misc.latex import latex
|
45
|
+
from sage.misc.lazy_import import lazy_import
|
46
|
+
from sage.rings.integer import Integer
|
47
|
+
from sage.sets.set import Set
|
48
|
+
|
49
|
+
lazy_import('sage.libs.gap.libgap', 'libgap')
|
50
|
+
|
51
|
+
|
52
|
+
class IncidenceStructure:
|
53
|
+
r"""
|
54
|
+
A base class for incidence structures (i.e. hypergraphs, i.e. set systems)
|
55
|
+
|
56
|
+
An incidence structure (i.e. hypergraph, i.e. set system) can be defined
|
57
|
+
from a collection of blocks (i.e. sets, i.e. edges), optionally with an
|
58
|
+
explicit ground set (i.e. point set, i.e. vertex set). Alternatively they
|
59
|
+
can be defined from a binary incidence matrix.
|
60
|
+
|
61
|
+
INPUT:
|
62
|
+
|
63
|
+
- ``points`` -- (i.e. ground set, i.e. vertex set) the underlying set. If
|
64
|
+
``points`` is an integer `v`, then the set is considered to be `\{0, ...,
|
65
|
+
v-1\}`.
|
66
|
+
|
67
|
+
.. NOTE::
|
68
|
+
|
69
|
+
The following syntax, where ``points`` is omitted, automatically
|
70
|
+
defines the ground set as the union of the blocks::
|
71
|
+
|
72
|
+
sage: H = IncidenceStructure([['a','b','c'],['c','d','e']])
|
73
|
+
sage: sorted(H.ground_set())
|
74
|
+
['a', 'b', 'c', 'd', 'e']
|
75
|
+
|
76
|
+
- ``blocks`` -- (i.e. edges, i.e. sets) the blocks defining the incidence
|
77
|
+
structure; can be any iterable
|
78
|
+
|
79
|
+
- ``incidence_matrix`` -- a binary incidence matrix; each column represents
|
80
|
+
a set
|
81
|
+
|
82
|
+
- ``name`` -- string (such as "Fano plane")
|
83
|
+
|
84
|
+
- ``check`` -- whether to check the input
|
85
|
+
|
86
|
+
- ``copy`` -- (use with caution) if set to ``False`` then ``blocks`` must be
|
87
|
+
a list of lists of integers. The list will not be copied but will be
|
88
|
+
modified in place (each block is sorted, and the whole list is
|
89
|
+
sorted). Your ``blocks`` object will become the
|
90
|
+
:class:`IncidenceStructure` instance's internal data.
|
91
|
+
|
92
|
+
EXAMPLES:
|
93
|
+
|
94
|
+
An incidence structure can be constructed by giving the number of points and
|
95
|
+
the list of blocks::
|
96
|
+
|
97
|
+
sage: IncidenceStructure(7, [[0,1,2],[0,3,4],[0,5,6],[1,3,5],[1,4,6],[2,3,6],[2,4,5]])
|
98
|
+
Incidence structure with 7 points and 7 blocks
|
99
|
+
|
100
|
+
Only providing the set of blocks is sufficient. In this case, the ground set
|
101
|
+
is defined as the union of the blocks::
|
102
|
+
|
103
|
+
sage: IncidenceStructure([[1,2,3],[2,3,4]])
|
104
|
+
Incidence structure with 4 points and 2 blocks
|
105
|
+
|
106
|
+
Or by its adjacency matrix (a `\{0,1\}`-matrix in which rows are indexed by
|
107
|
+
points and columns by blocks)::
|
108
|
+
|
109
|
+
sage: m = matrix([[0,1,0],[0,0,1],[1,0,1],[1,1,1]]) # needs sage.modules
|
110
|
+
sage: IncidenceStructure(m) # needs sage.modules
|
111
|
+
Incidence structure with 4 points and 3 blocks
|
112
|
+
|
113
|
+
The points can be any (hashable) object::
|
114
|
+
|
115
|
+
sage: V = [(0,'a'),(0,'b'),(1,'a'),(1,'b')]
|
116
|
+
sage: B = [(V[0],V[1],V[2]), (V[1],V[2]), (V[0],V[2])]
|
117
|
+
sage: I = IncidenceStructure(V, B)
|
118
|
+
sage: I.ground_set()
|
119
|
+
[(0, 'a'), (0, 'b'), (1, 'a'), (1, 'b')]
|
120
|
+
sage: I.blocks()
|
121
|
+
[[(0, 'a'), (0, 'b'), (1, 'a')], [(0, 'a'), (1, 'a')], [(0, 'b'), (1, 'a')]]
|
122
|
+
|
123
|
+
The order of the points and blocks does not matter as they are sorted on
|
124
|
+
input (see :issue:`11333`)::
|
125
|
+
|
126
|
+
sage: A = IncidenceStructure([0,1,2], [[0],[0,2]])
|
127
|
+
sage: B = IncidenceStructure([1,0,2], [[0],[2,0]])
|
128
|
+
sage: B == A
|
129
|
+
True
|
130
|
+
|
131
|
+
sage: C = BlockDesign(2, [[0], [1,0]])
|
132
|
+
sage: D = BlockDesign(2, [[0,1], [0]])
|
133
|
+
sage: C == D
|
134
|
+
True
|
135
|
+
|
136
|
+
If you care for speed, you can set ``copy`` to ``False``, but in that
|
137
|
+
case, your input must be a list of lists and the ground set must be `{0,
|
138
|
+
..., v-1}`::
|
139
|
+
|
140
|
+
sage: blocks = [[0,1],[2,0],[1,2]] # a list of lists of integers
|
141
|
+
sage: I = IncidenceStructure(3, blocks, copy=False)
|
142
|
+
sage: I._blocks is blocks
|
143
|
+
True
|
144
|
+
"""
|
145
|
+
def __init__(self, points=None, blocks=None, incidence_matrix=None,
|
146
|
+
name=None, check=True, copy=True):
|
147
|
+
r"""
|
148
|
+
TESTS::
|
149
|
+
|
150
|
+
sage: IncidenceStructure(3, [[4]])
|
151
|
+
Traceback (most recent call last):
|
152
|
+
...
|
153
|
+
ValueError: Block [4] is not contained in the point set
|
154
|
+
|
155
|
+
sage: IncidenceStructure(3, [[0,1],[0,2]], check=True)
|
156
|
+
Incidence structure with 3 points and 2 blocks
|
157
|
+
|
158
|
+
sage: IncidenceStructure(2, [[0,1,2,3,4,5]], check=False)
|
159
|
+
Incidence structure with 2 points and 1 blocks
|
160
|
+
|
161
|
+
We avoid to convert to integers when the points are not (but compare
|
162
|
+
equal to integers because of coercion)::
|
163
|
+
|
164
|
+
sage: # needs sage.rings.finite_rings
|
165
|
+
sage: V = GF(5)
|
166
|
+
sage: e0,e1,e2,e3,e4 = V
|
167
|
+
sage: [e0,e1,e2,e3,e4] == list(range(5)) # coercion makes them equal
|
168
|
+
True
|
169
|
+
sage: blocks = [[e0,e1,e2],[e0,e1],[e2,e4]]
|
170
|
+
sage: I = IncidenceStructure(V, blocks)
|
171
|
+
sage: type(I.ground_set()[0])
|
172
|
+
<class 'sage.rings.finite_rings.integer_mod.IntegerMod_int'>
|
173
|
+
sage: type(I.blocks()[0][0])
|
174
|
+
<class 'sage.rings.finite_rings.integer_mod.IntegerMod_int'>
|
175
|
+
|
176
|
+
TESTS::
|
177
|
+
|
178
|
+
sage: IncidenceStructure([])
|
179
|
+
Incidence structure with 0 points and 0 blocks
|
180
|
+
"""
|
181
|
+
from sage.structure.element import Matrix
|
182
|
+
|
183
|
+
# Reformatting input
|
184
|
+
if isinstance(points, Matrix):
|
185
|
+
assert incidence_matrix is None, "'incidence_matrix' cannot be defined when 'points' is a matrix"
|
186
|
+
assert blocks is None, "'blocks' cannot be defined when 'points' is a matrix"
|
187
|
+
incidence_matrix = points
|
188
|
+
points = blocks = None
|
189
|
+
elif points is not None and blocks is None:
|
190
|
+
blocks = points
|
191
|
+
points = set().union(*blocks)
|
192
|
+
if points:
|
193
|
+
assert incidence_matrix is None, "'incidence_matrix' cannot be defined when 'points' is defined"
|
194
|
+
|
195
|
+
if incidence_matrix:
|
196
|
+
from sage.matrix.constructor import matrix
|
197
|
+
M = matrix(incidence_matrix)
|
198
|
+
v = M.nrows()
|
199
|
+
self._points = list(range(v))
|
200
|
+
self._point_to_index = None
|
201
|
+
self._blocks = sorted(M.nonzero_positions_in_column(i) for i in range(M.ncols()))
|
202
|
+
|
203
|
+
else:
|
204
|
+
if isinstance(points, (int, Integer)):
|
205
|
+
self._points = list(range(points))
|
206
|
+
self._point_to_index = None
|
207
|
+
else:
|
208
|
+
self._points = list(points)
|
209
|
+
if self._points == list(range(len(points))) and all(isinstance(x, (int, Integer)) for x in self._points):
|
210
|
+
self._point_to_index = None
|
211
|
+
else:
|
212
|
+
self._point_to_index = {e: i for i, e in enumerate(self._points)}
|
213
|
+
|
214
|
+
if check:
|
215
|
+
for block in blocks:
|
216
|
+
if any(x not in self._points for x in block):
|
217
|
+
raise ValueError("Block {} is not contained in the point set".format(block))
|
218
|
+
if len(block) != len(set(block)):
|
219
|
+
raise ValueError("Repeated element in block {}".format(block))
|
220
|
+
|
221
|
+
if self._point_to_index:
|
222
|
+
# translate everything to integers between 0 and v-1
|
223
|
+
blocks = [sorted(self._point_to_index[e] for e in block) for block in blocks]
|
224
|
+
elif copy:
|
225
|
+
# create a new list made of sorted blocks
|
226
|
+
blocks = [sorted(block) for block in blocks]
|
227
|
+
else:
|
228
|
+
# sort the data but avoid copying it
|
229
|
+
for b in blocks:
|
230
|
+
b.sort()
|
231
|
+
|
232
|
+
blocks.sort()
|
233
|
+
self._blocks = blocks
|
234
|
+
|
235
|
+
self._name = str(name) if name is not None else 'IncidenceStructure'
|
236
|
+
self._classes = None
|
237
|
+
self._canonical_label = None
|
238
|
+
|
239
|
+
def __iter__(self):
|
240
|
+
"""
|
241
|
+
Iterator over the blocks.
|
242
|
+
|
243
|
+
EXAMPLES::
|
244
|
+
|
245
|
+
sage: sts = designs.steiner_triple_system(9)
|
246
|
+
sage: list(sts)
|
247
|
+
[[0, 1, 5], [0, 2, 4], [0, 3, 6], [0, 7, 8], [1, 2, 3], [1, 4, 7],
|
248
|
+
[1, 6, 8], [2, 5, 8], [2, 6, 7], [3, 4, 8], [3, 5, 7], [4, 5, 6]]
|
249
|
+
|
250
|
+
sage: b = IncidenceStructure('ab', ['a','ab'])
|
251
|
+
sage: it = iter(b)
|
252
|
+
sage: next(it)
|
253
|
+
['a']
|
254
|
+
sage: next(it)
|
255
|
+
['a', 'b']
|
256
|
+
"""
|
257
|
+
if self._point_to_index is None:
|
258
|
+
for b in self._blocks:
|
259
|
+
yield b[:]
|
260
|
+
else:
|
261
|
+
for b in self._blocks:
|
262
|
+
yield [self._points[i] for i in b]
|
263
|
+
|
264
|
+
def __repr__(self):
|
265
|
+
"""
|
266
|
+
A print method.
|
267
|
+
|
268
|
+
EXAMPLES::
|
269
|
+
|
270
|
+
sage: BD = IncidenceStructure(7,[[0,1,2],[0,3,4],[0,5,6],[1,3,5],[1,4,6],[2,3,6],[2,4,5]])
|
271
|
+
sage: BD
|
272
|
+
Incidence structure with 7 points and 7 blocks
|
273
|
+
"""
|
274
|
+
return 'Incidence structure with {} points and {} blocks'.format(
|
275
|
+
self.num_points(), self.num_blocks())
|
276
|
+
|
277
|
+
__str__ = __repr__
|
278
|
+
|
279
|
+
def __eq__(self, other):
|
280
|
+
"""
|
281
|
+
Test whether the two incidence structures are equal.
|
282
|
+
|
283
|
+
TESTS::
|
284
|
+
|
285
|
+
sage: blocks = [[0,1,2],[0,3,4],[0,5,6],[1,3,5],[1,4,6],[2,3,6],[2,4,5]]
|
286
|
+
sage: BD1 = IncidenceStructure(7, blocks)
|
287
|
+
sage: M = BD1.incidence_matrix() # needs sage.modules
|
288
|
+
sage: BD2 = IncidenceStructure(incidence_matrix=M) # needs sage.modules
|
289
|
+
sage: BD1 == BD2 # needs sage.modules
|
290
|
+
True
|
291
|
+
|
292
|
+
sage: e1 = frozenset([0,1])
|
293
|
+
sage: e2 = frozenset([2])
|
294
|
+
sage: sorted([e1,e2]) == [e1,e2]
|
295
|
+
True
|
296
|
+
sage: sorted([e2,e1]) == [e2,e1]
|
297
|
+
True
|
298
|
+
sage: I1 = IncidenceStructure([e1,e2], [[e1],[e1,e2]])
|
299
|
+
sage: I2 = IncidenceStructure([e1,e2], [[e2,e1],[e1]])
|
300
|
+
sage: I3 = IncidenceStructure([e2,e1], [[e1,e2],[e1]])
|
301
|
+
sage: I1 == I2 and I2 == I1 and I1 == I3 and I3 == I1 and I2 == I3 and I3 == I2
|
302
|
+
True
|
303
|
+
"""
|
304
|
+
# We are extra careful in this method since we cannot assume that a
|
305
|
+
# total order is defined on the point set.
|
306
|
+
if not isinstance(other, IncidenceStructure):
|
307
|
+
return False
|
308
|
+
|
309
|
+
if self._points == other._points:
|
310
|
+
return self._blocks == other._blocks
|
311
|
+
|
312
|
+
if (self.num_points() != other.num_points() or
|
313
|
+
self.num_blocks() != other.num_blocks()):
|
314
|
+
return False
|
315
|
+
|
316
|
+
p_to_i = self._point_to_index if self._point_to_index else list(range(self.num_points()))
|
317
|
+
|
318
|
+
if any(p not in p_to_i for p in other.ground_set()):
|
319
|
+
return False
|
320
|
+
|
321
|
+
other_blocks = sorted(sorted(p_to_i[p] for p in b) for b in other.blocks())
|
322
|
+
return self._blocks == other_blocks
|
323
|
+
|
324
|
+
def __ne__(self, other):
|
325
|
+
r"""
|
326
|
+
Difference test.
|
327
|
+
|
328
|
+
EXAMPLES::
|
329
|
+
|
330
|
+
sage: BD1 = IncidenceStructure(7, [[0,1,2],[0,3,4],[0,5,6],[1,3,5],[1,4,6],[2,3,6],[2,4,5]])
|
331
|
+
sage: M = BD1.incidence_matrix() # needs sage.modules
|
332
|
+
sage: BD2 = IncidenceStructure(incidence_matrix=M) # needs sage.modules
|
333
|
+
sage: BD1 != BD2 # needs sage.modules
|
334
|
+
False
|
335
|
+
"""
|
336
|
+
return not self == other
|
337
|
+
|
338
|
+
def __contains__(self, block):
|
339
|
+
r"""
|
340
|
+
Test if a block belongs to the incidence structure.
|
341
|
+
|
342
|
+
INPUT:
|
343
|
+
|
344
|
+
- ``block`` -- a block
|
345
|
+
|
346
|
+
EXAMPLES::
|
347
|
+
|
348
|
+
sage: [1,2,3,4] in IncidenceStructure([[1,2,3,4]])
|
349
|
+
True
|
350
|
+
sage: [1,2,4,3] in IncidenceStructure([[1,2,3,4]])
|
351
|
+
True
|
352
|
+
sage: [1,2,"3",4] in IncidenceStructure([[1,2,3,4]])
|
353
|
+
False
|
354
|
+
sage: [1,2,"3",4] in IncidenceStructure([[1,2,"3",4]])
|
355
|
+
True
|
356
|
+
|
357
|
+
More complicated examples::
|
358
|
+
|
359
|
+
sage: str="I had a dream of a time when a 3-lines patch does not kill one hour"
|
360
|
+
sage: sets = Subsets(str.split(), 4)
|
361
|
+
sage: IS = IncidenceStructure(sets) # a complete 4-uniform hypergraph
|
362
|
+
sage: ["I", "dream", "of", "one"] in IS
|
363
|
+
True
|
364
|
+
sage: ["does", "patch", "kill", "dream"] in IS
|
365
|
+
True
|
366
|
+
sage: ["Am", "I", "finally", "done ?"] in IS
|
367
|
+
False
|
368
|
+
sage: IS = designs.ProjectiveGeometryDesign(3, 1, GF(2), # needs sage.combinat sage.modules
|
369
|
+
....: point_coordinates=False)
|
370
|
+
sage: [3,8,7] in IS # needs sage.combinat sage.modules
|
371
|
+
True
|
372
|
+
sage: [3,8,9] in IS # needs sage.combinat sage.modules
|
373
|
+
False
|
374
|
+
"""
|
375
|
+
try:
|
376
|
+
iter(block)
|
377
|
+
except TypeError:
|
378
|
+
return False
|
379
|
+
|
380
|
+
# Relabel to 0,...,n-1 if necessary
|
381
|
+
if self._point_to_index is not None:
|
382
|
+
try:
|
383
|
+
block = [self._point_to_index[x] for x in block]
|
384
|
+
except KeyError:
|
385
|
+
return False
|
386
|
+
|
387
|
+
return sorted(block) in self._blocks
|
388
|
+
|
389
|
+
def canonical_label(self):
|
390
|
+
r"""
|
391
|
+
Return a canonical label for the incidence structure.
|
392
|
+
|
393
|
+
A canonical label is relabeling of the points into integers
|
394
|
+
`\{0,...,n-1\}` such that isomorphic incidence structures are
|
395
|
+
relabelled to equal objects.
|
396
|
+
|
397
|
+
EXAMPLES::
|
398
|
+
|
399
|
+
sage: # needs sage.schemes
|
400
|
+
sage: fano1 = designs.balanced_incomplete_block_design(7,3)
|
401
|
+
sage: fano2 = designs.projective_plane(2)
|
402
|
+
sage: fano1 == fano2
|
403
|
+
False
|
404
|
+
sage: fano1.relabel(fano1.canonical_label())
|
405
|
+
sage: fano2.relabel(fano2.canonical_label())
|
406
|
+
sage: fano1 == fano2
|
407
|
+
True
|
408
|
+
"""
|
409
|
+
if self._canonical_label is None:
|
410
|
+
from sage.graphs.graph import Graph
|
411
|
+
g = Graph()
|
412
|
+
n = self.num_points()
|
413
|
+
g.add_edges((i+n, x) for i, b in enumerate(self._blocks) for x in b)
|
414
|
+
canonical_label = g.canonical_label([list(range(n)), list(range(n, n+self.num_blocks()))], certificate=True)[1]
|
415
|
+
canonical_label = [canonical_label[x] for x in range(n)]
|
416
|
+
self._canonical_label = canonical_label
|
417
|
+
|
418
|
+
return dict(zip(self._points, self._canonical_label))
|
419
|
+
|
420
|
+
def is_isomorphic(self, other, certificate=False):
|
421
|
+
r"""
|
422
|
+
Return whether the two incidence structures are isomorphic.
|
423
|
+
|
424
|
+
INPUT:
|
425
|
+
|
426
|
+
- ``other`` -- an incidence structure
|
427
|
+
|
428
|
+
- ``certificate`` -- boolean (default: ``False``); whether to return an
|
429
|
+
isomorphism from ``self`` to ``other`` instead of a boolean answer
|
430
|
+
|
431
|
+
EXAMPLES::
|
432
|
+
|
433
|
+
sage: # needs sage.schemes
|
434
|
+
sage: fano1 = designs.balanced_incomplete_block_design(7,3)
|
435
|
+
sage: fano2 = designs.projective_plane(2)
|
436
|
+
sage: fano1.is_isomorphic(fano2)
|
437
|
+
True
|
438
|
+
sage: fano1.is_isomorphic(fano2,certificate=True)
|
439
|
+
{0: 0, 1: 1, 2: 2, 3: 6, 4: 4, 5: 3, 6: 5}
|
440
|
+
|
441
|
+
TESTS::
|
442
|
+
|
443
|
+
sage: # needs sage.symbolic
|
444
|
+
sage: IS = IncidenceStructure([["A",5,pi],["A",5,"Wouhou"],
|
445
|
+
....: ["A","Wouhou",(9,9)],[pi,12]])
|
446
|
+
sage: IS2 = IS.copy()
|
447
|
+
sage: IS2.relabel(IS2.canonical_label())
|
448
|
+
sage: IS.is_isomorphic(IS2)
|
449
|
+
True
|
450
|
+
sage: canon = IS.is_isomorphic(IS2, certificate=True)
|
451
|
+
sage: IS.relabel(canon)
|
452
|
+
sage: IS==IS2
|
453
|
+
True
|
454
|
+
|
455
|
+
sage: IS2 = IncidenceStructure([[1,2]])
|
456
|
+
sage: IS2.is_isomorphic(IS) # needs sage.symbolic
|
457
|
+
False
|
458
|
+
sage: IS2.is_isomorphic(IS, certificate=True) # needs sage.symbolic
|
459
|
+
{}
|
460
|
+
|
461
|
+
Checking whether two :class:`IncidenceStructure` are isomorphic
|
462
|
+
incidentally computes their canonical label (if necessary). Thus,
|
463
|
+
subsequent calls to :meth:`is_isomorphic` will be faster::
|
464
|
+
|
465
|
+
sage: # needs sage.schemes
|
466
|
+
sage: IS1 = designs.projective_plane(3)
|
467
|
+
sage: IS2 = IS1.relabel(Permutations(IS1.ground_set()).random_element(),
|
468
|
+
....: inplace=False)
|
469
|
+
sage: IS2 = IncidenceStructure(IS2.blocks())
|
470
|
+
sage: IS1._canonical_label is None and IS2._canonical_label is None
|
471
|
+
True
|
472
|
+
sage: IS1.is_isomorphic(IS2)
|
473
|
+
True
|
474
|
+
sage: IS1._canonical_label is None or IS2._canonical_label is None
|
475
|
+
False
|
476
|
+
"""
|
477
|
+
if (self.num_points() != other.num_points() or
|
478
|
+
self.num_blocks() != other.num_blocks() or
|
479
|
+
sorted(self.block_sizes()) != sorted(other.block_sizes())):
|
480
|
+
return {} if certificate else False
|
481
|
+
|
482
|
+
A_canon = self.canonical_label()
|
483
|
+
B_canon = other.canonical_label()
|
484
|
+
|
485
|
+
A = self.relabel(A_canon, inplace=False)
|
486
|
+
B = other.relabel(B_canon, inplace=False)
|
487
|
+
|
488
|
+
if A == B:
|
489
|
+
if certificate:
|
490
|
+
B_canon_rev = {y: x for x, y in B_canon.items()}
|
491
|
+
return {x: B_canon_rev[xint] for x, xint in A_canon.items()}
|
492
|
+
else:
|
493
|
+
return True
|
494
|
+
else:
|
495
|
+
return {} if certificate else False
|
496
|
+
|
497
|
+
def isomorphic_substructures_iterator(self, H2, induced=False):
|
498
|
+
r"""
|
499
|
+
Iterate over all copies of ``H2`` contained in ``self``.
|
500
|
+
|
501
|
+
A hypergraph `H_1` contains an isomorphic copy of a hypergraph `H_2` if
|
502
|
+
there exists an injection `f:V(H_2)\mapsto V(H_1)` such that for any set
|
503
|
+
`S_2\in E(H_2)` the set `S_1=f(S2)` belongs to `E(H_1)`.
|
504
|
+
|
505
|
+
It is an *induced* copy if no other set of `E(H_1)` is contained in
|
506
|
+
`f(V(H_2))`, i.e. `|E(H_2)|=\{S:S\in E(H_1)\text{ and }f(V(H_2))\}`.
|
507
|
+
|
508
|
+
This function lists all such injections. In particular, the number of
|
509
|
+
copies of `H` in itself is equal to *the size of its automorphism
|
510
|
+
group*.
|
511
|
+
|
512
|
+
See :mod:`~sage.combinat.designs.subhypergraph_search` for more information.
|
513
|
+
|
514
|
+
INPUT:
|
515
|
+
|
516
|
+
- ``H2`` -- an :class:`IncidenceStructure` object
|
517
|
+
|
518
|
+
- ``induced`` -- boolean (default: ``False``); whether to require the copies to be
|
519
|
+
induced
|
520
|
+
|
521
|
+
EXAMPLES:
|
522
|
+
|
523
|
+
How many distinct `C_5` in Petersen's graph ? ::
|
524
|
+
|
525
|
+
sage: P = graphs.PetersenGraph()
|
526
|
+
sage: C = graphs.CycleGraph(5)
|
527
|
+
sage: IP = IncidenceStructure(P.edges(sort=True, labels=False))
|
528
|
+
sage: IC = IncidenceStructure(C.edges(sort=True, labels=False))
|
529
|
+
sage: sum(1 for _ in IP.isomorphic_substructures_iterator(IC))
|
530
|
+
120
|
531
|
+
|
532
|
+
As the automorphism group of `C_5` has size 10, the number of distinct
|
533
|
+
unlabelled copies is 12. Let us check that all functions returned
|
534
|
+
correspond to an actual `C_5` subgraph::
|
535
|
+
|
536
|
+
sage: for f in IP.isomorphic_substructures_iterator(IC):
|
537
|
+
....: assert all(P.has_edge(f[x],f[y]) for x,y in C.edges(sort=True, labels=False))
|
538
|
+
|
539
|
+
The number of induced copies, in this case, is the same::
|
540
|
+
|
541
|
+
sage: sum(1 for _ in IP.isomorphic_substructures_iterator(IC,induced=True))
|
542
|
+
120
|
543
|
+
|
544
|
+
They begin to differ if we make one vertex universal::
|
545
|
+
|
546
|
+
sage: P.add_edges([(0,x) for x in P], loops=False)
|
547
|
+
sage: IP = IncidenceStructure(P.edges(sort=True, labels=False))
|
548
|
+
sage: IC = IncidenceStructure(C.edges(sort=True, labels=False))
|
549
|
+
sage: sum(1 for _ in IP.isomorphic_substructures_iterator(IC))
|
550
|
+
420
|
551
|
+
sage: sum(1 for _ in IP.isomorphic_substructures_iterator(IC,induced=True))
|
552
|
+
60
|
553
|
+
|
554
|
+
The number of copies of `H` in itself is the size of its automorphism
|
555
|
+
group::
|
556
|
+
|
557
|
+
sage: H = designs.projective_plane(3) # needs sage.schemes
|
558
|
+
sage: sum(1 for _ in H.isomorphic_substructures_iterator(H)) # needs sage.schemes
|
559
|
+
5616
|
560
|
+
sage: H.automorphism_group().cardinality() # needs sage.groups sage.schemes
|
561
|
+
5616
|
562
|
+
"""
|
563
|
+
from sage.combinat.designs.subhypergraph_search import SubHypergraphSearch
|
564
|
+
return SubHypergraphSearch(self, H2, induced=induced)
|
565
|
+
|
566
|
+
def copy(self):
|
567
|
+
r"""
|
568
|
+
Return a copy of the incidence structure.
|
569
|
+
|
570
|
+
EXAMPLES::
|
571
|
+
|
572
|
+
sage: IS = IncidenceStructure([[1,2,3,"e"]], name='Test')
|
573
|
+
sage: IS
|
574
|
+
Incidence structure with 4 points and 1 blocks
|
575
|
+
sage: copy(IS)
|
576
|
+
Incidence structure with 4 points and 1 blocks
|
577
|
+
sage: [1, 2, 3, 'e'] in copy(IS)
|
578
|
+
True
|
579
|
+
sage: copy(IS)._name
|
580
|
+
'Test'
|
581
|
+
"""
|
582
|
+
IS = IncidenceStructure(self._blocks,
|
583
|
+
name=self._name,
|
584
|
+
check=False)
|
585
|
+
IS.relabel(dict(zip(range(self.num_points()), self._points)))
|
586
|
+
IS._canonical_label = None if self._canonical_label is None else self._canonical_label[:]
|
587
|
+
|
588
|
+
return IS
|
589
|
+
|
590
|
+
__copy__ = copy
|
591
|
+
|
592
|
+
def induced_substructure(self, points):
|
593
|
+
r"""
|
594
|
+
Return the substructure induced by a set of points.
|
595
|
+
|
596
|
+
The substructure induced in `\mathcal H` by a set `X\subseteq V(\mathcal
|
597
|
+
H)` of points is the incidence structure `\mathcal H_X` defined on `X`
|
598
|
+
whose sets are all `S\in \mathcal H` such that `S\subseteq X`.
|
599
|
+
|
600
|
+
INPUT:
|
601
|
+
|
602
|
+
- ``points`` -- set of points
|
603
|
+
|
604
|
+
.. NOTE::
|
605
|
+
|
606
|
+
This method goes over all sets of ``self`` before building a new
|
607
|
+
:class:`IncidenceStructure` (which involves some relabelling and
|
608
|
+
sorting). It probably should not be called in a performance-critical
|
609
|
+
code.
|
610
|
+
|
611
|
+
EXAMPLES:
|
612
|
+
|
613
|
+
A Fano plane with one point removed::
|
614
|
+
|
615
|
+
sage: F = designs.steiner_triple_system(7)
|
616
|
+
sage: F.induced_substructure([0..5])
|
617
|
+
Incidence structure with 6 points and 4 blocks
|
618
|
+
|
619
|
+
TESTS::
|
620
|
+
|
621
|
+
sage: F.induced_substructure([0..50])
|
622
|
+
Traceback (most recent call last):
|
623
|
+
...
|
624
|
+
ValueError: 7 is not a point of the incidence structure
|
625
|
+
sage: F.relabel(dict(enumerate("abcdefg")))
|
626
|
+
sage: F.induced_substructure("abc")
|
627
|
+
Incidence structure with 3 points and ...
|
628
|
+
sage: F.induced_substructure("Y")
|
629
|
+
Traceback (most recent call last):
|
630
|
+
...
|
631
|
+
ValueError: 'Y' is not a point of the incidence structure
|
632
|
+
"""
|
633
|
+
# Checking the input
|
634
|
+
if self._point_to_index is None:
|
635
|
+
n = self.num_points()
|
636
|
+
for x in points:
|
637
|
+
x = int(x)
|
638
|
+
if x < 0 or x >= n:
|
639
|
+
raise ValueError("{} is not a point of the incidence structure".format(x))
|
640
|
+
int_points = points
|
641
|
+
else:
|
642
|
+
try:
|
643
|
+
int_points = [self._point_to_index[x] for x in points]
|
644
|
+
except KeyError as bad_pt:
|
645
|
+
raise ValueError("{} is not a point of the incidence structure".format(bad_pt))
|
646
|
+
|
647
|
+
int_points = set(int_points)
|
648
|
+
return IncidenceStructure(points,
|
649
|
+
[[self._points[x] for x in S]
|
650
|
+
for S in self._blocks
|
651
|
+
if int_points.issuperset(S)])
|
652
|
+
|
653
|
+
def trace(self, points, min_size=1, multiset=True):
|
654
|
+
r"""
|
655
|
+
Return the trace of a set of points.
|
656
|
+
|
657
|
+
Given an hypergraph `\mathcal H`, the *trace* of a set `X` of points in
|
658
|
+
`\mathcal H` is the hypergraph whose blocks are all non-empty `S \cap X`
|
659
|
+
where `S \in \mathcal H`.
|
660
|
+
|
661
|
+
INPUT:
|
662
|
+
|
663
|
+
- ``points`` -- set of points
|
664
|
+
|
665
|
+
- ``min_size`` -- integer (default: 1); minimum size of the sets to
|
666
|
+
keep. By default all empty sets are discarded, i.e. ``min_size=1``
|
667
|
+
|
668
|
+
- ``multiset`` -- boolean (default: ``True``); whether to keep multiple
|
669
|
+
copies of the same set
|
670
|
+
|
671
|
+
.. NOTE::
|
672
|
+
|
673
|
+
This method goes over all sets of ``self`` before building a new
|
674
|
+
:class:`IncidenceStructure` (which involves some relabelling and
|
675
|
+
sorting). It probably should not be called in a performance-critical
|
676
|
+
code.
|
677
|
+
|
678
|
+
EXAMPLES:
|
679
|
+
|
680
|
+
A Baer subplane of order 2 (i.e. a Fano plane) in a projective plane of order 4::
|
681
|
+
|
682
|
+
sage: # needs sage.schemes
|
683
|
+
sage: P4 = designs.projective_plane(4)
|
684
|
+
sage: F = designs.projective_plane(2)
|
685
|
+
sage: for x in Subsets(P4.ground_set(),7):
|
686
|
+
....: if P4.trace(x,min_size=2).is_isomorphic(F):
|
687
|
+
....: break
|
688
|
+
sage: subplane = P4.trace(x,min_size=2); subplane
|
689
|
+
Incidence structure with 7 points and 7 blocks
|
690
|
+
sage: subplane.is_isomorphic(F)
|
691
|
+
True
|
692
|
+
|
693
|
+
TESTS::
|
694
|
+
|
695
|
+
sage: # needs sage.schemes
|
696
|
+
sage: F.trace([0..50])
|
697
|
+
Traceback (most recent call last):
|
698
|
+
...
|
699
|
+
ValueError: 7 is not a point of the incidence structure
|
700
|
+
sage: F.relabel(dict(enumerate("abcdefg")))
|
701
|
+
sage: F.trace("abc")
|
702
|
+
Incidence structure with 3 points and ...
|
703
|
+
sage: F.trace("Y")
|
704
|
+
Traceback (most recent call last):
|
705
|
+
...
|
706
|
+
ValueError: 'Y' is not a point of the incidence structure
|
707
|
+
"""
|
708
|
+
# Checking the input
|
709
|
+
if self._point_to_index is None:
|
710
|
+
n = self.num_points()
|
711
|
+
int_points = frozenset(int(x) for x in points)
|
712
|
+
for x in int_points:
|
713
|
+
if x < 0 or x >= n:
|
714
|
+
raise ValueError("{} is not a point of the incidence structure".format(x))
|
715
|
+
else:
|
716
|
+
try:
|
717
|
+
int_points = frozenset(self._point_to_index[x] for x in points)
|
718
|
+
except KeyError as bad_pt:
|
719
|
+
raise ValueError("{} is not a point of the incidence structure".format(bad_pt))
|
720
|
+
|
721
|
+
blocks = [int_points.intersection(S) for S in self._blocks]
|
722
|
+
if min_size:
|
723
|
+
blocks = [S for S in blocks if len(S) >= min_size]
|
724
|
+
if not multiset:
|
725
|
+
blocks = set(blocks)
|
726
|
+
IS = IncidenceStructure(blocks)
|
727
|
+
IS.relabel({i: self._points[i] for i in int_points})
|
728
|
+
return IS
|
729
|
+
|
730
|
+
def ground_set(self):
|
731
|
+
r"""
|
732
|
+
Return the ground set (i.e the list of points).
|
733
|
+
|
734
|
+
EXAMPLES::
|
735
|
+
|
736
|
+
sage: IncidenceStructure(3, [[0,1],[0,2]]).ground_set()
|
737
|
+
[0, 1, 2]
|
738
|
+
"""
|
739
|
+
return self._points[:]
|
740
|
+
|
741
|
+
def num_points(self):
|
742
|
+
r"""
|
743
|
+
Return the size of the ground set.
|
744
|
+
|
745
|
+
EXAMPLES::
|
746
|
+
|
747
|
+
sage: designs.DesarguesianProjectivePlaneDesign(2).num_points()
|
748
|
+
7
|
749
|
+
sage: B = IncidenceStructure(4, [[0,1],[0,2],[0,3],[1,2], [1,2,3]])
|
750
|
+
sage: B.num_points()
|
751
|
+
4
|
752
|
+
"""
|
753
|
+
return len(self._points)
|
754
|
+
|
755
|
+
def num_blocks(self):
|
756
|
+
r"""
|
757
|
+
Return the number of blocks.
|
758
|
+
|
759
|
+
EXAMPLES::
|
760
|
+
|
761
|
+
sage: designs.DesarguesianProjectivePlaneDesign(2).num_blocks()
|
762
|
+
7
|
763
|
+
sage: B = IncidenceStructure(4, [[0,1],[0,2],[0,3],[1,2], [1,2,3]])
|
764
|
+
sage: B.num_blocks()
|
765
|
+
5
|
766
|
+
"""
|
767
|
+
return len(self._blocks)
|
768
|
+
|
769
|
+
def blocks(self):
|
770
|
+
"""
|
771
|
+
Return the list of blocks.
|
772
|
+
|
773
|
+
EXAMPLES::
|
774
|
+
|
775
|
+
sage: BD = IncidenceStructure(7,[[0,1,2],[0,3,4],[0,5,6],[1,3,5],[1,4,6],[2,3,6],[2,4,5]])
|
776
|
+
sage: BD.blocks()
|
777
|
+
[[0, 1, 2], [0, 3, 4], [0, 5, 6], [1, 3, 5], [1, 4, 6], [2, 3, 6], [2, 4, 5]]
|
778
|
+
"""
|
779
|
+
if self._point_to_index is None:
|
780
|
+
return [b[:] for b in self._blocks]
|
781
|
+
else:
|
782
|
+
return [[self._points[i] for i in b] for b in self._blocks]
|
783
|
+
|
784
|
+
def block_sizes(self):
|
785
|
+
r"""
|
786
|
+
Return the set of block sizes.
|
787
|
+
|
788
|
+
EXAMPLES::
|
789
|
+
|
790
|
+
sage: BD = IncidenceStructure(8, [[0,1,3],[1,4,5,6],[1,2],[5,6,7]])
|
791
|
+
sage: BD.block_sizes()
|
792
|
+
[3, 2, 4, 3]
|
793
|
+
sage: BD = IncidenceStructure(7,[[0,1,2],[0,3,4],[0,5,6],[1,3,5],[1,4,6],[2,3,6],[2,4,5]])
|
794
|
+
sage: BD.block_sizes()
|
795
|
+
[3, 3, 3, 3, 3, 3, 3]
|
796
|
+
"""
|
797
|
+
return [len(b) for b in self._blocks]
|
798
|
+
|
799
|
+
def degree(self, p=None, subset=False):
|
800
|
+
r"""
|
801
|
+
Return the degree of a point ``p`` (or a set of points).
|
802
|
+
|
803
|
+
The degree of a point (or set of points) is the number of blocks that
|
804
|
+
contain it.
|
805
|
+
|
806
|
+
INPUT:
|
807
|
+
|
808
|
+
- ``p`` -- a point (or a set of points) of the incidence structure
|
809
|
+
|
810
|
+
- ``subset`` -- boolean (default: ``False``); whether to interpret the
|
811
|
+
argument as a set of point or as a point (default)
|
812
|
+
|
813
|
+
EXAMPLES::
|
814
|
+
|
815
|
+
sage: designs.steiner_triple_system(9).degree(3)
|
816
|
+
4
|
817
|
+
sage: designs.steiner_triple_system(9).degree({1,2},subset=True)
|
818
|
+
1
|
819
|
+
|
820
|
+
TESTS::
|
821
|
+
|
822
|
+
sage: designs.steiner_triple_system(9).degree(subset=True)
|
823
|
+
Traceback (most recent call last):
|
824
|
+
...
|
825
|
+
ValueError: subset must be False when p is None
|
826
|
+
"""
|
827
|
+
if p is None:
|
828
|
+
if subset is True:
|
829
|
+
raise ValueError("subset must be False when p is None")
|
830
|
+
|
831
|
+
# degree of a point
|
832
|
+
if not subset:
|
833
|
+
if self._point_to_index:
|
834
|
+
p = self._point_to_index.get(p, -1)
|
835
|
+
else:
|
836
|
+
p = p if (p >= 0 and p < len(self._points)) else -1
|
837
|
+
return sum((p in b) for b in self._blocks) if p != -1 else 0
|
838
|
+
|
839
|
+
# degree of a set
|
840
|
+
else:
|
841
|
+
if self._point_to_index:
|
842
|
+
p = set(self._point_to_index.get(x, -1) for x in p)
|
843
|
+
else:
|
844
|
+
p = set(p) if all(x >= 0 and x < len(self._points) for x in p) else set([-1])
|
845
|
+
|
846
|
+
return sum(p.issubset(b) for b in self._blocks) if -1 not in p else 0
|
847
|
+
|
848
|
+
def degrees(self, size=None):
|
849
|
+
r"""
|
850
|
+
Return the degree of all sets of given size, or the degree of all points.
|
851
|
+
|
852
|
+
The degree of a point (or set of point) is the number of blocks that
|
853
|
+
contain it.
|
854
|
+
|
855
|
+
INPUT:
|
856
|
+
|
857
|
+
- ``size`` -- integer; return the degree of all subsets of points of
|
858
|
+
cardinality ``size``. When ``size=None``, the function outputs the
|
859
|
+
degree of all points.
|
860
|
+
|
861
|
+
.. NOTE::
|
862
|
+
|
863
|
+
When ``size=None`` the output is indexed by the points. When
|
864
|
+
``size=1`` it is indexed by tuples of size 1. This is the same
|
865
|
+
information, stored slightly differently.
|
866
|
+
|
867
|
+
OUTPUT: a dictionary whose values are degrees and keys are either:
|
868
|
+
|
869
|
+
- the points of the incidence structure if ``size=None`` (default)
|
870
|
+
|
871
|
+
- the subsets of size ``size`` of the points stored as tuples
|
872
|
+
|
873
|
+
EXAMPLES::
|
874
|
+
|
875
|
+
sage: IncidenceStructure([[1,2,3],[1,4]]).degrees(2)
|
876
|
+
{(1, 2): 1, (1, 3): 1, (1, 4): 1, (2, 3): 1, (2, 4): 0, (3, 4): 0}
|
877
|
+
|
878
|
+
In a Steiner triple system, all pairs have degree 1::
|
879
|
+
|
880
|
+
sage: S13 = designs.steiner_triple_system(13)
|
881
|
+
sage: all(v == 1 for v in S13.degrees(2).values())
|
882
|
+
True
|
883
|
+
"""
|
884
|
+
if size is None:
|
885
|
+
d = [0]*self.num_points()
|
886
|
+
for b in self._blocks:
|
887
|
+
for x in b:
|
888
|
+
d[x] += 1
|
889
|
+
return {p: d[i] for i, p in enumerate(self._points)}
|
890
|
+
else:
|
891
|
+
from itertools import combinations
|
892
|
+
d = {t: 0 for t in combinations(range(self.num_points()), size)}
|
893
|
+
for b in self._blocks:
|
894
|
+
for s in combinations(b, size):
|
895
|
+
d[s] += 1
|
896
|
+
if self._point_to_index:
|
897
|
+
return {tuple([self._points[x] for x in s]): v for s, v in d.items()}
|
898
|
+
else:
|
899
|
+
return d
|
900
|
+
|
901
|
+
def rank(self):
|
902
|
+
r"""
|
903
|
+
Return the rank of the hypergraph (the maximum size of a block).
|
904
|
+
|
905
|
+
EXAMPLES::
|
906
|
+
|
907
|
+
sage: h = Hypergraph(8, [[0,1,3],[1,4,5,6],[1,2]])
|
908
|
+
sage: h.rank()
|
909
|
+
4
|
910
|
+
"""
|
911
|
+
return max(len(b) for b in self._blocks)
|
912
|
+
|
913
|
+
def is_regular(self, r=None) -> bool | int:
|
914
|
+
r"""
|
915
|
+
Test whether the incidence structure is `r`-regular.
|
916
|
+
|
917
|
+
An incidence structure is said to be `r`-regular if all its points are
|
918
|
+
incident with exactly `r` blocks.
|
919
|
+
|
920
|
+
INPUT:
|
921
|
+
|
922
|
+
- ``r`` -- integer
|
923
|
+
|
924
|
+
OUTPUT:
|
925
|
+
|
926
|
+
If ``r`` is defined, a boolean is returned. If ``r`` is set to ``None``
|
927
|
+
(default), the method returns either ``False`` or the integer ``r`` such
|
928
|
+
that the incidence structure is `r`-regular.
|
929
|
+
|
930
|
+
.. WARNING::
|
931
|
+
|
932
|
+
In case of `0`-regular incidence structure, beware that ``if not
|
933
|
+
H.is_regular()`` is a satisfied condition.
|
934
|
+
|
935
|
+
EXAMPLES::
|
936
|
+
|
937
|
+
sage: designs.balanced_incomplete_block_design(7,3).is_regular() # needs sage.schemes
|
938
|
+
3
|
939
|
+
sage: designs.balanced_incomplete_block_design(7,3).is_regular(r=3) # needs sage.schemes
|
940
|
+
True
|
941
|
+
sage: designs.balanced_incomplete_block_design(7,3).is_regular(r=4) # needs sage.schemes
|
942
|
+
False
|
943
|
+
|
944
|
+
TESTS::
|
945
|
+
|
946
|
+
sage: IncidenceStructure([]).is_regular()
|
947
|
+
Traceback (most recent call last):
|
948
|
+
...
|
949
|
+
ValueError: This incidence structure has no points.
|
950
|
+
"""
|
951
|
+
if self.num_points() == 0:
|
952
|
+
raise ValueError("This incidence structure has no points.")
|
953
|
+
count = [0] * self.num_points()
|
954
|
+
for b in self._blocks:
|
955
|
+
for x in b:
|
956
|
+
count[x] += 1
|
957
|
+
scount = set(count)
|
958
|
+
if len(scount) != 1:
|
959
|
+
return False
|
960
|
+
if r is None:
|
961
|
+
return scount.pop()
|
962
|
+
return scount.pop() == r
|
963
|
+
|
964
|
+
def is_uniform(self, k=None) -> bool | int:
|
965
|
+
r"""
|
966
|
+
Test whether the incidence structure is `k`-uniform
|
967
|
+
|
968
|
+
An incidence structure is said to be `k`-uniform if all its blocks have
|
969
|
+
size `k`.
|
970
|
+
|
971
|
+
INPUT:
|
972
|
+
|
973
|
+
- ``k`` -- integer
|
974
|
+
|
975
|
+
OUTPUT:
|
976
|
+
|
977
|
+
If ``k`` is defined, a boolean is returned. If ``k`` is set to ``None``
|
978
|
+
(default), the method returns either ``False`` or the integer ``k`` such
|
979
|
+
that the incidence structure is `k`-uniform.
|
980
|
+
|
981
|
+
.. WARNING::
|
982
|
+
|
983
|
+
In case of `0`-uniform incidence structure, beware that ``if not
|
984
|
+
H.is_uniform()`` is a satisfied condition.
|
985
|
+
|
986
|
+
EXAMPLES::
|
987
|
+
|
988
|
+
sage: designs.balanced_incomplete_block_design(7,3).is_uniform() # needs sage.schemes
|
989
|
+
3
|
990
|
+
sage: designs.balanced_incomplete_block_design(7,3).is_uniform(k=3) # needs sage.schemes
|
991
|
+
True
|
992
|
+
sage: designs.balanced_incomplete_block_design(7,3).is_uniform(k=4) # needs sage.schemes
|
993
|
+
False
|
994
|
+
|
995
|
+
TESTS::
|
996
|
+
|
997
|
+
sage: IncidenceStructure([]).is_uniform()
|
998
|
+
Traceback (most recent call last):
|
999
|
+
...
|
1000
|
+
ValueError: This incidence structure has no blocks.
|
1001
|
+
"""
|
1002
|
+
if self.num_blocks() == 0:
|
1003
|
+
raise ValueError("This incidence structure has no blocks.")
|
1004
|
+
sizes = set(self.block_sizes())
|
1005
|
+
if len(sizes) != 1:
|
1006
|
+
return False
|
1007
|
+
if k is None:
|
1008
|
+
return sizes.pop()
|
1009
|
+
return sizes.pop() == k
|
1010
|
+
|
1011
|
+
def is_connected(self) -> bool:
|
1012
|
+
r"""
|
1013
|
+
Test whether the design is connected.
|
1014
|
+
|
1015
|
+
EXAMPLES::
|
1016
|
+
|
1017
|
+
sage: IncidenceStructure(3, [[0,1],[0,2]]).is_connected()
|
1018
|
+
True
|
1019
|
+
sage: IncidenceStructure(4, [[0,1],[2,3]]).is_connected()
|
1020
|
+
False
|
1021
|
+
"""
|
1022
|
+
from sage.sets.disjoint_set import DisjointSet
|
1023
|
+
D = DisjointSet(self.num_points())
|
1024
|
+
for B in self._blocks:
|
1025
|
+
x = B[0]
|
1026
|
+
for i in range(1, len(B)):
|
1027
|
+
D.union(x, B[i])
|
1028
|
+
return D.number_of_subsets() == 1
|
1029
|
+
|
1030
|
+
def is_simple(self) -> bool:
|
1031
|
+
r"""
|
1032
|
+
Test whether this design is simple (i.e. no repeated block).
|
1033
|
+
|
1034
|
+
EXAMPLES::
|
1035
|
+
|
1036
|
+
sage: IncidenceStructure(3, [[0,1],[1,2],[0,2]]).is_simple()
|
1037
|
+
True
|
1038
|
+
sage: IncidenceStructure(3, [[0],[0]]).is_simple()
|
1039
|
+
False
|
1040
|
+
|
1041
|
+
sage: V = [(0,'a'),(0,'b'),(1,'a'),(1,'b')]
|
1042
|
+
sage: B = [[V[0],V[1]], [V[1],V[2]]]
|
1043
|
+
sage: I = IncidenceStructure(V, B)
|
1044
|
+
sage: I.is_simple()
|
1045
|
+
True
|
1046
|
+
sage: I2 = IncidenceStructure(V, B*2)
|
1047
|
+
sage: I2.is_simple()
|
1048
|
+
False
|
1049
|
+
"""
|
1050
|
+
B = self._blocks
|
1051
|
+
return all(B[i] != B[i + 1] for i in range(len(B) - 1))
|
1052
|
+
|
1053
|
+
def _gap_(self):
|
1054
|
+
"""
|
1055
|
+
Return the GAP string describing the design.
|
1056
|
+
|
1057
|
+
EXAMPLES::
|
1058
|
+
|
1059
|
+
sage: BD = IncidenceStructure(7,[[0,1,2],[0,3,4],[0,5,6],[1,3,5],[1,4,6],[2,3,6],[2,4,5]])
|
1060
|
+
sage: BD._gap_()
|
1061
|
+
'BlockDesign(7,[[1, 2, 3], [1, 4, 5], [1, 6, 7], [2, 4, 6], [2, 5, 7], [3, 4, 7], [3, 5, 6]])'
|
1062
|
+
"""
|
1063
|
+
v = self.num_points()
|
1064
|
+
gB = [[x + 1 for x in b] for b in self._blocks]
|
1065
|
+
return "BlockDesign({},{})".format(v, gB)
|
1066
|
+
|
1067
|
+
def _libgap_(self):
|
1068
|
+
"""
|
1069
|
+
Return the design as a GAP record.
|
1070
|
+
|
1071
|
+
EXAMPLES::
|
1072
|
+
|
1073
|
+
sage: D = IncidenceStructure(4, [[0,2],[1,2,3],[2,3]])
|
1074
|
+
sage: D._libgap_() # optional - gap_package_design
|
1075
|
+
rec( blocks := [ [ 1, 3 ], [ 2, 3, 4 ], [ 3, 4 ] ],
|
1076
|
+
isBlockDesign := true, v := 4 )
|
1077
|
+
"""
|
1078
|
+
libgap.load_package("design")
|
1079
|
+
v = self.num_points()
|
1080
|
+
gB = [[x + 1 for x in b] for b in self._blocks]
|
1081
|
+
return libgap.BlockDesign(v, gB)
|
1082
|
+
|
1083
|
+
def intersection_graph(self, sizes=None):
|
1084
|
+
r"""
|
1085
|
+
Return the intersection graph of the incidence structure.
|
1086
|
+
|
1087
|
+
The vertices of this graph are the :meth:`blocks` of the incidence
|
1088
|
+
structure. Two of them are adjacent if the size of their intersection
|
1089
|
+
belongs to the set ``sizes``.
|
1090
|
+
|
1091
|
+
INPUT:
|
1092
|
+
|
1093
|
+
- ``sizes`` -- list/set of integers; for convenience, setting
|
1094
|
+
``sizes`` to ``5`` has the same effect as ``sizes=[5]``. When set to
|
1095
|
+
``None`` (default), behaves as ``sizes=PositiveIntegers()``.
|
1096
|
+
|
1097
|
+
EXAMPLES:
|
1098
|
+
|
1099
|
+
The intersection graph of a
|
1100
|
+
:func:`~sage.combinat.designs.bibd.balanced_incomplete_block_design` is
|
1101
|
+
a :meth:`strongly regular graph <Graph.is_strongly_regular>` (when it is
|
1102
|
+
not trivial)::
|
1103
|
+
|
1104
|
+
sage: BIBD = designs.balanced_incomplete_block_design(19,3)
|
1105
|
+
sage: G = BIBD.intersection_graph(1)
|
1106
|
+
sage: G.is_strongly_regular(parameters=True)
|
1107
|
+
(57, 24, 11, 9)
|
1108
|
+
"""
|
1109
|
+
from sage.sets.positive_integers import PositiveIntegers
|
1110
|
+
from sage.graphs.graph import Graph
|
1111
|
+
from sage.sets.set import Set
|
1112
|
+
if sizes is None:
|
1113
|
+
sizes = PositiveIntegers()
|
1114
|
+
elif sizes in PositiveIntegers():
|
1115
|
+
sizes = (sizes,)
|
1116
|
+
V = [Set(v) for v in self]
|
1117
|
+
return Graph([V, lambda x, y: len(x & y) in sizes], loops=False)
|
1118
|
+
|
1119
|
+
def incidence_matrix(self):
|
1120
|
+
r"""
|
1121
|
+
Return the incidence matrix `A` of the design. A is a `(v \times b)`
|
1122
|
+
matrix defined by: ``A[i,j] = 1`` if ``i`` is in block ``B_j`` and 0
|
1123
|
+
otherwise.
|
1124
|
+
|
1125
|
+
EXAMPLES::
|
1126
|
+
|
1127
|
+
sage: BD = IncidenceStructure(7, [[0,1,2],[0,3,4],[0,5,6],[1,3,5],
|
1128
|
+
....: [1,4,6],[2,3,6],[2,4,5]])
|
1129
|
+
sage: BD.block_sizes()
|
1130
|
+
[3, 3, 3, 3, 3, 3, 3]
|
1131
|
+
sage: BD.incidence_matrix() # needs sage.modules
|
1132
|
+
[1 1 1 0 0 0 0]
|
1133
|
+
[1 0 0 1 1 0 0]
|
1134
|
+
[1 0 0 0 0 1 1]
|
1135
|
+
[0 1 0 1 0 1 0]
|
1136
|
+
[0 1 0 0 1 0 1]
|
1137
|
+
[0 0 1 1 0 0 1]
|
1138
|
+
[0 0 1 0 1 1 0]
|
1139
|
+
|
1140
|
+
sage: I = IncidenceStructure('abc', ('ab','abc','ac','c'))
|
1141
|
+
sage: I.incidence_matrix() # needs sage.modules
|
1142
|
+
[1 1 1 0]
|
1143
|
+
[1 1 0 0]
|
1144
|
+
[0 1 1 1]
|
1145
|
+
"""
|
1146
|
+
from sage.matrix.constructor import matrix
|
1147
|
+
from sage.rings.integer_ring import ZZ
|
1148
|
+
A = matrix(ZZ, self.num_points(), self.num_blocks(), sparse=True)
|
1149
|
+
for j, b in enumerate(self._blocks):
|
1150
|
+
for i in b:
|
1151
|
+
A[i, j] = 1
|
1152
|
+
return A
|
1153
|
+
|
1154
|
+
def incidence_graph(self, labels=False):
|
1155
|
+
r"""
|
1156
|
+
Return the incidence graph of the incidence structure.
|
1157
|
+
|
1158
|
+
A point and a block are adjacent in this graph whenever they are
|
1159
|
+
incident.
|
1160
|
+
|
1161
|
+
INPUT:
|
1162
|
+
|
1163
|
+
- ``labels`` -- boolean; whether to return a graph whose vertices are
|
1164
|
+
integers, or labelled elements
|
1165
|
+
|
1166
|
+
- ``labels is False`` -- default; in this case the first vertices
|
1167
|
+
of the graphs are the elements of :meth:`ground_set`, and appear
|
1168
|
+
in the same order. Similarly, the following vertices represent the
|
1169
|
+
elements of :meth:`blocks`, and appear in the same order.
|
1170
|
+
|
1171
|
+
- ``labels is True``, the points keep their original labels, and the
|
1172
|
+
blocks are :func:`Set <Set>` objects.
|
1173
|
+
|
1174
|
+
Note that the labelled incidence graph can be incorrect when
|
1175
|
+
blocks are repeated, and on some (rare) occasions when the
|
1176
|
+
elements of :meth:`ground_set` mix :func:`Set` and non-:func:`Set
|
1177
|
+
<Set>` objects.
|
1178
|
+
|
1179
|
+
EXAMPLES::
|
1180
|
+
|
1181
|
+
sage: BD = IncidenceStructure(7, [[0,1,2],[0,3,4],[0,5,6],[1,3,5],
|
1182
|
+
....: [1,4,6],[2,3,6],[2,4,5]])
|
1183
|
+
sage: BD.incidence_graph() # needs sage.modules
|
1184
|
+
Bipartite graph on 14 vertices
|
1185
|
+
sage: A = BD.incidence_matrix() # needs sage.modules
|
1186
|
+
sage: Graph(block_matrix([[A*0, A], # needs sage.modules
|
1187
|
+
....: [A.transpose(),A*0]])) == BD.incidence_graph()
|
1188
|
+
True
|
1189
|
+
|
1190
|
+
TESTS:
|
1191
|
+
|
1192
|
+
With ``labels = True``::
|
1193
|
+
|
1194
|
+
sage: BD.incidence_graph(labels=True).has_edge(0,Set([0,1,2]))
|
1195
|
+
True
|
1196
|
+
"""
|
1197
|
+
if labels:
|
1198
|
+
from sage.graphs.graph import Graph
|
1199
|
+
from sage.sets.set import Set
|
1200
|
+
G = Graph()
|
1201
|
+
G.add_vertices(self.ground_set())
|
1202
|
+
for b in self.blocks():
|
1203
|
+
b = Set(b)
|
1204
|
+
G.add_vertex(b)
|
1205
|
+
G.add_edges((b, x) for x in b)
|
1206
|
+
return G
|
1207
|
+
|
1208
|
+
else:
|
1209
|
+
from sage.graphs.bipartite_graph import BipartiteGraph
|
1210
|
+
A = self.incidence_matrix()
|
1211
|
+
return BipartiteGraph(A)
|
1212
|
+
|
1213
|
+
def is_berge_cyclic(self):
|
1214
|
+
r"""
|
1215
|
+
Check whether ``self`` is a Berge-Cyclic uniform hypergraph.
|
1216
|
+
|
1217
|
+
A `k`-uniform Berge cycle (named after Claude Berge) of length `\ell`
|
1218
|
+
is a cyclic list of distinct `k`-sets `F_1,\ldots,F_\ell`, `\ell>1`,
|
1219
|
+
and distinct vertices `C = \{v_1,\ldots,v_\ell\}` such that for each
|
1220
|
+
`1\le i\le \ell`, `F_i` contains `v_i` and `v_{i+1}` (where `v_{l+1} =
|
1221
|
+
v_1`).
|
1222
|
+
|
1223
|
+
A uniform hypergraph is Berge-cyclic if its incidence graph is cyclic.
|
1224
|
+
It is called "Berge-acyclic" otherwise.
|
1225
|
+
|
1226
|
+
For more information, see [Fag1983]_ and :wikipedia:`Hypergraph`.
|
1227
|
+
|
1228
|
+
EXAMPLES::
|
1229
|
+
|
1230
|
+
sage: Hypergraph(5, [[1, 2, 3], [2, 3, 4]]).is_berge_cyclic() # needs sage.modules
|
1231
|
+
True
|
1232
|
+
sage: Hypergraph(6, [[1, 2, 3], [3, 4, 5]]).is_berge_cyclic() # needs sage.modules
|
1233
|
+
False
|
1234
|
+
|
1235
|
+
TESTS::
|
1236
|
+
|
1237
|
+
sage: Hypergraph(5, [[1, 2, 3], [2, 3]]).is_berge_cyclic()
|
1238
|
+
Traceback (most recent call last):
|
1239
|
+
...
|
1240
|
+
TypeError: Berge cycles are defined for uniform hypergraphs only
|
1241
|
+
"""
|
1242
|
+
if not self.is_uniform():
|
1243
|
+
raise TypeError("Berge cycles are defined for uniform hypergraphs only")
|
1244
|
+
|
1245
|
+
return not self.incidence_graph().is_forest()
|
1246
|
+
|
1247
|
+
def complement(self, uniform=False):
|
1248
|
+
r"""
|
1249
|
+
Return the complement of the incidence structure.
|
1250
|
+
|
1251
|
+
Two different definitions of "complement" are made available, according
|
1252
|
+
to the value of ``uniform``.
|
1253
|
+
|
1254
|
+
INPUT:
|
1255
|
+
|
1256
|
+
- ``uniform`` -- boolean
|
1257
|
+
|
1258
|
+
- if set to ``False`` (default), returns the incidence structure whose
|
1259
|
+
blocks are the complements of all blocks of the incidence structure.
|
1260
|
+
|
1261
|
+
- If set to ``True`` and the incidence structure is `k`-uniform,
|
1262
|
+
returns the incidence structure whose blocks are all `k`-sets of the
|
1263
|
+
ground set that do not appear in ``self``.
|
1264
|
+
|
1265
|
+
EXAMPLES:
|
1266
|
+
|
1267
|
+
The complement of a
|
1268
|
+
:class:`~sage.combinat.designs.bibd.BalancedIncompleteBlockDesign` is
|
1269
|
+
also a `2`-design::
|
1270
|
+
|
1271
|
+
sage: bibd = designs.balanced_incomplete_block_design(13,4) # needs sage.schemes
|
1272
|
+
sage: bibd.is_t_design(return_parameters=True) # needs sage.schemes
|
1273
|
+
(True, (2, 13, 4, 1))
|
1274
|
+
sage: bibd.complement().is_t_design(return_parameters=True) # needs sage.schemes
|
1275
|
+
(True, (2, 13, 9, 6))
|
1276
|
+
|
1277
|
+
The "uniform" complement of a graph is a graph::
|
1278
|
+
|
1279
|
+
sage: g = graphs.PetersenGraph()
|
1280
|
+
sage: G = IncidenceStructure(g.edges(sort=True, labels=False))
|
1281
|
+
sage: H = G.complement(uniform=True)
|
1282
|
+
sage: h = Graph(H.blocks())
|
1283
|
+
sage: g == h
|
1284
|
+
False
|
1285
|
+
sage: g == h.complement()
|
1286
|
+
True
|
1287
|
+
|
1288
|
+
TESTS::
|
1289
|
+
|
1290
|
+
sage: bibd.relabel({i:str(i) for i in bibd.ground_set()}) # needs sage.schemes
|
1291
|
+
sage: bibd.complement().ground_set() # needs sage.schemes
|
1292
|
+
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']
|
1293
|
+
|
1294
|
+
sage: I = IncidenceStructure('abc', ['ab','ac','bc'])
|
1295
|
+
sage: I.is_t_design(return_parameters=True)
|
1296
|
+
(True, (2, 3, 2, 1))
|
1297
|
+
"""
|
1298
|
+
if uniform:
|
1299
|
+
k = self.is_uniform()
|
1300
|
+
if k is False:
|
1301
|
+
raise ValueError("The incidence structure is not uniform.")
|
1302
|
+
|
1303
|
+
blocks = []
|
1304
|
+
num_blocks = self.num_blocks()
|
1305
|
+
i = 0
|
1306
|
+
from itertools import combinations
|
1307
|
+
for B in combinations(range(self.num_points()), k):
|
1308
|
+
B = list(B)
|
1309
|
+
while i < num_blocks and self._blocks[i] < B:
|
1310
|
+
i += 1
|
1311
|
+
if i < num_blocks and self._blocks[i] == B:
|
1312
|
+
i += 1
|
1313
|
+
continue
|
1314
|
+
blocks.append(B)
|
1315
|
+
I = IncidenceStructure(blocks, copy=False)
|
1316
|
+
else:
|
1317
|
+
X = set(range(self.num_points()))
|
1318
|
+
I = IncidenceStructure([X.difference(B) for B in self._blocks])
|
1319
|
+
|
1320
|
+
I.relabel({i: self._points[i] for i in range(self.num_points())})
|
1321
|
+
return I
|
1322
|
+
|
1323
|
+
def relabel(self, perm=None, inplace=True):
|
1324
|
+
r"""
|
1325
|
+
Relabel the ground set.
|
1326
|
+
|
1327
|
+
INPUT:
|
1328
|
+
|
1329
|
+
- ``perm`` -- can be one of
|
1330
|
+
|
1331
|
+
- a dictionary -- then each point ``p`` (which should be a key of
|
1332
|
+
``d``) is relabeled to ``d[p]``
|
1333
|
+
|
1334
|
+
- a list or a tuple of length ``n`` -- the first point returned by
|
1335
|
+
:meth:`ground_set` is relabeled to ``l[0]``, the second to
|
1336
|
+
``l[1]``, ...
|
1337
|
+
|
1338
|
+
- ``None`` -- the incidence structure is relabeled to be on
|
1339
|
+
`\{0,1,...,n-1\}` in the ordering given by :meth:`ground_set`
|
1340
|
+
|
1341
|
+
- ``inplace`` -- boolean (default: ``False``); if ``True`` then return
|
1342
|
+
a relabeled graph and does not touch ``self``
|
1343
|
+
|
1344
|
+
EXAMPLES::
|
1345
|
+
|
1346
|
+
sage: # needs sage.schemes
|
1347
|
+
sage: TD = designs.transversal_design(5,5)
|
1348
|
+
sage: TD.relabel({i: chr(97+i) for i in range(25)})
|
1349
|
+
sage: TD.ground_set()
|
1350
|
+
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
|
1351
|
+
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y']
|
1352
|
+
sage: TD.blocks()[:3]
|
1353
|
+
[['a', 'f', 'k', 'p', 'u'], ['a', 'g', 'm', 's', 'y'], ['a', 'h', 'o', 'q', 'x']]
|
1354
|
+
|
1355
|
+
Relabel to integer points::
|
1356
|
+
|
1357
|
+
sage: TD.relabel() # needs sage.schemes
|
1358
|
+
sage: TD.blocks()[:3] # needs sage.schemes
|
1359
|
+
[[0, 5, 10, 15, 20], [0, 6, 12, 18, 24], [0, 7, 14, 16, 23]]
|
1360
|
+
|
1361
|
+
TESTS:
|
1362
|
+
|
1363
|
+
Check that the relabel is consistent on a fixed incidence structure::
|
1364
|
+
|
1365
|
+
sage: I = IncidenceStructure([0,1,2,3,4],
|
1366
|
+
....: [[0,1,3],[0,2,4],[2,3,4],[0,1]])
|
1367
|
+
sage: I.relabel()
|
1368
|
+
sage: from itertools import permutations
|
1369
|
+
sage: for p in permutations([0,1,2,3,4]):
|
1370
|
+
....: J = I.relabel(p,inplace=False)
|
1371
|
+
....: if I == J: print(p)
|
1372
|
+
(0, 1, 2, 3, 4)
|
1373
|
+
(0, 1, 4, 3, 2)
|
1374
|
+
|
1375
|
+
And one can also verify that we have exactly two automorphisms::
|
1376
|
+
|
1377
|
+
sage: I.automorphism_group() # needs sage.groups
|
1378
|
+
Permutation Group with generators [(2,4)]
|
1379
|
+
"""
|
1380
|
+
if not inplace:
|
1381
|
+
from copy import copy
|
1382
|
+
G = copy(self)
|
1383
|
+
G.relabel(perm=perm, inplace=True)
|
1384
|
+
return G
|
1385
|
+
|
1386
|
+
if perm is None:
|
1387
|
+
self._points = list(range(self.num_points()))
|
1388
|
+
self._point_to_index = None
|
1389
|
+
return
|
1390
|
+
|
1391
|
+
if isinstance(perm, (list, tuple)):
|
1392
|
+
perm = dict(zip(self._points, perm))
|
1393
|
+
|
1394
|
+
if not isinstance(perm, dict):
|
1395
|
+
raise ValueError("perm argument must be None, a list or a dictionary")
|
1396
|
+
|
1397
|
+
if len(set(perm.values())) != len(perm):
|
1398
|
+
raise ValueError("two points are getting relabelled with the same name")
|
1399
|
+
|
1400
|
+
self._points = [perm[x] for x in self._points]
|
1401
|
+
if self._points == list(range(self.num_points())):
|
1402
|
+
self._point_to_index = None
|
1403
|
+
else:
|
1404
|
+
self._point_to_index = {v: i for i, v in enumerate(self._points)}
|
1405
|
+
|
1406
|
+
# __hash__ = None
|
1407
|
+
# This object is mutable because of .relabel()
|
1408
|
+
|
1409
|
+
#####################
|
1410
|
+
# real computations #
|
1411
|
+
#####################
|
1412
|
+
|
1413
|
+
def packing(self, solver=None, verbose=0, *, integrality_tolerance=1e-3):
|
1414
|
+
r"""
|
1415
|
+
Return a maximum packing.
|
1416
|
+
|
1417
|
+
A maximum packing in a hypergraph is collection of disjoint sets/blocks
|
1418
|
+
of maximal cardinality. This problem is NP-complete in general, and in
|
1419
|
+
particular on 3-uniform hypergraphs. It is solved here with an Integer
|
1420
|
+
Linear Program.
|
1421
|
+
|
1422
|
+
For more information, see the :wikipedia:`Packing_in_a_hypergraph`.
|
1423
|
+
|
1424
|
+
INPUT:
|
1425
|
+
|
1426
|
+
- ``solver`` -- (default: ``None``) specify a Mixed Integer Linear
|
1427
|
+
Programming (MILP) solver to be used. If set to ``None``, the default
|
1428
|
+
one is used. For more information on LP solvers and which default
|
1429
|
+
solver is used, see the method :meth:`solve
|
1430
|
+
<sage.numerical.mip.MixedIntegerLinearProgram.solve>` of the class
|
1431
|
+
:class:`MixedIntegerLinearProgram
|
1432
|
+
<sage.numerical.mip.MixedIntegerLinearProgram>`.
|
1433
|
+
|
1434
|
+
- ``verbose`` -- integer (default: 0); sets the level of
|
1435
|
+
verbosity. Set to 0 by default, which means quiet.
|
1436
|
+
|
1437
|
+
- ``integrality_tolerance`` -- parameter for use with MILP solvers over
|
1438
|
+
an inexact base ring; see
|
1439
|
+
:meth:`MixedIntegerLinearProgram.get_values`.
|
1440
|
+
|
1441
|
+
EXAMPLES::
|
1442
|
+
|
1443
|
+
sage: P = IncidenceStructure([[1,2],[3,4],[2,3]]).packing() # needs sage.numerical.mip
|
1444
|
+
sage: sorted(sorted(b) for b in P) # needs sage.numerical.mip
|
1445
|
+
[[1, 2], [3, 4]]
|
1446
|
+
sage: len(designs.steiner_triple_system(9).packing()) # needs sage.numerical.mip
|
1447
|
+
3
|
1448
|
+
"""
|
1449
|
+
from sage.numerical.mip import MixedIntegerLinearProgram
|
1450
|
+
|
1451
|
+
# List of blocks containing a given point x
|
1452
|
+
d = [[] for _ in self._points]
|
1453
|
+
for i, B in enumerate(self._blocks):
|
1454
|
+
for x in B:
|
1455
|
+
d[x].append(i)
|
1456
|
+
|
1457
|
+
p = MixedIntegerLinearProgram(solver=solver)
|
1458
|
+
b = p.new_variable(binary=True)
|
1459
|
+
for x, L in enumerate(d): # Set of disjoint blocks
|
1460
|
+
p.add_constraint(p.sum([b[i] for i in L]) <= 1)
|
1461
|
+
|
1462
|
+
# Maximum number of blocks
|
1463
|
+
p.set_objective(p.sum([b[i] for i in range(self.num_blocks())]))
|
1464
|
+
|
1465
|
+
p.solve(log=verbose)
|
1466
|
+
|
1467
|
+
values = p.get_values(b, convert=bool, tolerance=integrality_tolerance)
|
1468
|
+
return [[self._points[x] for x in self._blocks[i]]
|
1469
|
+
for i, v in values.items() if v]
|
1470
|
+
|
1471
|
+
def is_t_design(self, t=None, v=None, k=None, l=None, return_parameters=False):
|
1472
|
+
r"""
|
1473
|
+
Test whether ``self`` is a `t-(v,k,l)` design.
|
1474
|
+
|
1475
|
+
A `t-(v,k,\lambda)` (sometimes called `t`-design for short) is a block
|
1476
|
+
design in which:
|
1477
|
+
|
1478
|
+
- the underlying set has cardinality `v`
|
1479
|
+
- the blocks have size `k`
|
1480
|
+
- each `t`-subset of points is covered by `\lambda` blocks
|
1481
|
+
|
1482
|
+
INPUT:
|
1483
|
+
|
1484
|
+
- ``t``, ``v``, ``k``, ``l`` -- integers; their value is set to
|
1485
|
+
``None`` by default. The function tests whether the design is a
|
1486
|
+
`t-(v,k,l)` design using the provided values and guesses the
|
1487
|
+
others. Note that ``l`` cannot be specified if ``t`` is not.
|
1488
|
+
|
1489
|
+
- ``return_parameters`` -- boolean; whether to return the parameters of
|
1490
|
+
the `t`-design. If set to ``True``, the function returns a pair
|
1491
|
+
``(boolean_answer,(t,v,k,l))``.
|
1492
|
+
|
1493
|
+
EXAMPLES::
|
1494
|
+
|
1495
|
+
sage: fano_blocks = [[0,1,2],[0,3,4],[0,5,6],[1,3,5],[1,4,6],[2,3,6],[2,4,5]]
|
1496
|
+
sage: BD = IncidenceStructure(7, fano_blocks)
|
1497
|
+
sage: BD.is_t_design()
|
1498
|
+
True
|
1499
|
+
sage: BD.is_t_design(return_parameters=True)
|
1500
|
+
(True, (2, 7, 3, 1))
|
1501
|
+
sage: BD.is_t_design(2, 7, 3, 1)
|
1502
|
+
True
|
1503
|
+
sage: BD.is_t_design(1, 7, 3, 3)
|
1504
|
+
True
|
1505
|
+
sage: BD.is_t_design(0, 7, 3, 7)
|
1506
|
+
True
|
1507
|
+
|
1508
|
+
sage: BD.is_t_design(0,6,3,7) or BD.is_t_design(0,7,4,7) or BD.is_t_design(0,7,3,8)
|
1509
|
+
False
|
1510
|
+
|
1511
|
+
sage: BD = designs.AffineGeometryDesign(3, 1, GF(2)) # needs sage.combinat sage.modules
|
1512
|
+
sage: BD.is_t_design(1) # needs sage.combinat sage.modules
|
1513
|
+
True
|
1514
|
+
sage: BD.is_t_design(2) # needs sage.combinat sage.modules
|
1515
|
+
True
|
1516
|
+
|
1517
|
+
Steiner triple and quadruple systems are other names for `2-(v,3,1)` and
|
1518
|
+
`3-(v,4,1)` designs::
|
1519
|
+
|
1520
|
+
sage: S3_9 = designs.steiner_triple_system(9)
|
1521
|
+
sage: S3_9.is_t_design(2,9,3,1)
|
1522
|
+
True
|
1523
|
+
|
1524
|
+
sage: blocks = designs.steiner_quadruple_system(8)
|
1525
|
+
sage: S4_8 = IncidenceStructure(8, blocks)
|
1526
|
+
sage: S4_8.is_t_design(3,8,4,1)
|
1527
|
+
True
|
1528
|
+
|
1529
|
+
sage: blocks = designs.steiner_quadruple_system(14)
|
1530
|
+
sage: S4_14 = IncidenceStructure(14, blocks)
|
1531
|
+
sage: S4_14.is_t_design(3,14,4,1)
|
1532
|
+
True
|
1533
|
+
|
1534
|
+
Some examples of Witt designs that need the gap database::
|
1535
|
+
|
1536
|
+
sage: # optional - gap_package_design
|
1537
|
+
sage: BD = designs.WittDesign(9)
|
1538
|
+
sage: BD.is_t_design(2,9,3,1)
|
1539
|
+
True
|
1540
|
+
sage: W12 = designs.WittDesign(12)
|
1541
|
+
sage: W12.is_t_design(5,12,6,1)
|
1542
|
+
True
|
1543
|
+
sage: W12.is_t_design(4)
|
1544
|
+
True
|
1545
|
+
|
1546
|
+
Further examples::
|
1547
|
+
|
1548
|
+
sage: D = IncidenceStructure(4,[[],[]])
|
1549
|
+
sage: D.is_t_design(return_parameters=True)
|
1550
|
+
(True, (0, 4, 0, 2))
|
1551
|
+
|
1552
|
+
sage: D = IncidenceStructure(4, [[0,1],[0,2],[0,3]])
|
1553
|
+
sage: D.is_t_design(return_parameters=True)
|
1554
|
+
(True, (0, 4, 2, 3))
|
1555
|
+
|
1556
|
+
sage: D = IncidenceStructure(4, [[0],[1],[2],[3]])
|
1557
|
+
sage: D.is_t_design(return_parameters=True)
|
1558
|
+
(True, (1, 4, 1, 1))
|
1559
|
+
|
1560
|
+
sage: D = IncidenceStructure(4,[[0,1],[2,3]])
|
1561
|
+
sage: D.is_t_design(return_parameters=True)
|
1562
|
+
(True, (1, 4, 2, 1))
|
1563
|
+
|
1564
|
+
sage: D = IncidenceStructure(4, [list(range(4))])
|
1565
|
+
sage: D.is_t_design(return_parameters=True)
|
1566
|
+
(True, (4, 4, 4, 1))
|
1567
|
+
|
1568
|
+
TESTS::
|
1569
|
+
|
1570
|
+
sage: blocks = designs.steiner_quadruple_system(8)
|
1571
|
+
sage: S4_8 = IncidenceStructure(8, blocks)
|
1572
|
+
sage: R = list(range(15))
|
1573
|
+
sage: [(v,k,l) for v in R for k in R for l in R if S4_8.is_t_design(3,v,k,l)]
|
1574
|
+
[(8, 4, 1)]
|
1575
|
+
sage: [(v,k,l) for v in R for k in R for l in R if S4_8.is_t_design(2,v,k,l)]
|
1576
|
+
[(8, 4, 3)]
|
1577
|
+
sage: [(v,k,l) for v in R for k in R for l in R if S4_8.is_t_design(1,v,k,l)]
|
1578
|
+
[(8, 4, 7)]
|
1579
|
+
sage: [(v,k,l) for v in R for k in R for l in R if S4_8.is_t_design(0,v,k,l)]
|
1580
|
+
[(8, 4, 14)]
|
1581
|
+
|
1582
|
+
sage: # needs sage.rings.finite_rings
|
1583
|
+
sage: A = designs.AffineGeometryDesign(3, 1, GF(2))
|
1584
|
+
sage: A.is_t_design(return_parameters=True)
|
1585
|
+
(True, (2, 8, 2, 1))
|
1586
|
+
sage: A = designs.AffineGeometryDesign(4, 2, GF(2))
|
1587
|
+
sage: A.is_t_design(return_parameters=True)
|
1588
|
+
(True, (3, 16, 4, 1))
|
1589
|
+
|
1590
|
+
sage: I = IncidenceStructure(2, [])
|
1591
|
+
sage: I.is_t_design(return_parameters=True)
|
1592
|
+
(True, (0, 2, 0, 0))
|
1593
|
+
sage: I = IncidenceStructure(2, [[0],[0,1]])
|
1594
|
+
sage: I.is_t_design(return_parameters=True)
|
1595
|
+
(False, (0, 0, 0, 0))
|
1596
|
+
|
1597
|
+
Verify that :issue:`38454` is fixed::
|
1598
|
+
|
1599
|
+
sage: I = IncidenceStructure(points=[0,1,2,3,4,5],
|
1600
|
+
....: blocks=[[0,1], [1,2], [0,2]])
|
1601
|
+
sage: I.is_t_design(return_parameters=True)
|
1602
|
+
(True, (0, 6, 2, 3))
|
1603
|
+
"""
|
1604
|
+
from sage.arith.misc import binomial
|
1605
|
+
|
1606
|
+
# Missing parameters ?
|
1607
|
+
if v is None:
|
1608
|
+
v = self.num_points()
|
1609
|
+
|
1610
|
+
if k is None:
|
1611
|
+
k = len(self._blocks[0]) if self._blocks else 0
|
1612
|
+
|
1613
|
+
if l is not None and t is None:
|
1614
|
+
raise ValueError("t must be set when l=None")
|
1615
|
+
|
1616
|
+
b = self.num_blocks()
|
1617
|
+
|
1618
|
+
# Trivial wrong answers
|
1619
|
+
if (any(len(block) != k for block in self._blocks) or # non k-uniform
|
1620
|
+
v != self.num_points()):
|
1621
|
+
return (False, (0, 0, 0, 0)) if return_parameters else False
|
1622
|
+
|
1623
|
+
# Trivial case t>k
|
1624
|
+
if (t is not None and t > k):
|
1625
|
+
if (l is None or l == 0):
|
1626
|
+
return (True, (t, v, k, 0)) if return_parameters else True
|
1627
|
+
else:
|
1628
|
+
return (False, (0, 0, 0, 0)) if return_parameters else False
|
1629
|
+
|
1630
|
+
# Trivial case k=0
|
1631
|
+
if k == 0:
|
1632
|
+
if (l is None or l == 0):
|
1633
|
+
return (True, (0, v, k, b)) if return_parameters else True
|
1634
|
+
else:
|
1635
|
+
return (False, (0, 0, 0, 0)) if return_parameters else False
|
1636
|
+
|
1637
|
+
# Trivial case k=v (includes v=0)
|
1638
|
+
if k == v:
|
1639
|
+
if t is None:
|
1640
|
+
t = v
|
1641
|
+
if l is None or b == l:
|
1642
|
+
return (True, (t, v, k, b)) if return_parameters else True
|
1643
|
+
else:
|
1644
|
+
return (True, (0, 0, 0, 0)) if return_parameters else False
|
1645
|
+
|
1646
|
+
# Handbook of combinatorial design theorem II.4.8:
|
1647
|
+
#
|
1648
|
+
# a t-(v,k,l) is also a t'-(v,k,l')
|
1649
|
+
# for t' < t and l' = l* binomial(v-t',t-t') / binomial(k-t',t-t')
|
1650
|
+
#
|
1651
|
+
# We look for the largest t such that self is a t-design
|
1652
|
+
from itertools import combinations
|
1653
|
+
for tt in (range(1, k + 1) if t is None else [t]):
|
1654
|
+
# is lambda an integer?
|
1655
|
+
if (b * binomial(k, tt)) % binomial(v, tt):
|
1656
|
+
tt -= 1
|
1657
|
+
break
|
1658
|
+
|
1659
|
+
s = {}
|
1660
|
+
for block in self._blocks:
|
1661
|
+
for i in combinations(block, tt):
|
1662
|
+
s[i] = s.get(i, 0) + 1
|
1663
|
+
|
1664
|
+
if (len(s) != binomial(v, tt)) or (len(set(s.values())) != 1):
|
1665
|
+
tt -= 1
|
1666
|
+
break
|
1667
|
+
|
1668
|
+
ll = (b * binomial(k, tt)) // binomial(v, tt)
|
1669
|
+
|
1670
|
+
if ((t is not None and t != tt) or
|
1671
|
+
(l is not None and l != ll)):
|
1672
|
+
return (False, (0, 0, 0, 0)) if return_parameters else False
|
1673
|
+
else:
|
1674
|
+
if tt == 0:
|
1675
|
+
ll = b
|
1676
|
+
return (True, (tt, v, k, ll)) if return_parameters else True
|
1677
|
+
|
1678
|
+
def is_generalized_quadrangle(self, verbose=False, parameters=False):
|
1679
|
+
r"""
|
1680
|
+
Test if the incidence structure is a generalized quadrangle.
|
1681
|
+
|
1682
|
+
An incidence structure is a generalized quadrangle iff (see [BH2012]_,
|
1683
|
+
section 9.6):
|
1684
|
+
|
1685
|
+
- two blocks intersect on at most one point.
|
1686
|
+
|
1687
|
+
- For every point `p` not in a block `B`, there is a unique block `B'`
|
1688
|
+
intersecting both `\{p\}` and `B`
|
1689
|
+
|
1690
|
+
It is a *regular* generalized quadrangle if furthermore:
|
1691
|
+
|
1692
|
+
- it is `s+1`-:meth:`uniform <is_uniform>` for some positive integer `s`.
|
1693
|
+
|
1694
|
+
- it is `t+1`-:meth:`regular <is_regular>` for some positive integer `t`.
|
1695
|
+
|
1696
|
+
For more information, see the :wikipedia:`Generalized_quadrangle`.
|
1697
|
+
|
1698
|
+
.. NOTE::
|
1699
|
+
|
1700
|
+
Some references (e.g. [PT2009]_ or
|
1701
|
+
:wikipedia:`Generalized_quadrangle`) only allow *regular*
|
1702
|
+
generalized quadrangles. To use such a definition, see the
|
1703
|
+
``parameters`` optional argument described below, or the methods
|
1704
|
+
:meth:`is_regular` and :meth:`is_uniform`.
|
1705
|
+
|
1706
|
+
INPUT:
|
1707
|
+
|
1708
|
+
- ``verbose`` -- boolean; whether to print an explanation when the
|
1709
|
+
instance is not a generalized quadrangle
|
1710
|
+
|
1711
|
+
- ``parameters`` -- (boolean; ``False``); if set to ``True``, the
|
1712
|
+
function returns a pair ``(s,t)`` instead of ``True`` answers. In this
|
1713
|
+
case, `s` and `t` are the integers defined above if they exist (each
|
1714
|
+
can be set to ``False`` otherwise).
|
1715
|
+
|
1716
|
+
EXAMPLES::
|
1717
|
+
|
1718
|
+
sage: h = designs.CremonaRichmondConfiguration() # needs networkx
|
1719
|
+
sage: h.is_generalized_quadrangle() # needs networkx
|
1720
|
+
True
|
1721
|
+
|
1722
|
+
This is actually a *regular* generalized quadrangle::
|
1723
|
+
|
1724
|
+
sage: h.is_generalized_quadrangle(parameters=True) # needs networkx
|
1725
|
+
(2, 2)
|
1726
|
+
|
1727
|
+
TESTS::
|
1728
|
+
|
1729
|
+
sage: H = IncidenceStructure((2*graphs.CompleteGraph(3)).edges(sort=True, labels=False))
|
1730
|
+
sage: H.is_generalized_quadrangle(verbose=True) # needs sage.modules
|
1731
|
+
Some point is at distance >3 from some block.
|
1732
|
+
False
|
1733
|
+
|
1734
|
+
sage: G = graphs.CycleGraph(5)
|
1735
|
+
sage: B = list(G.subgraph_search_iterator(graphs.PathGraph(3), # needs sage.modules
|
1736
|
+
....: return_graphs=False))
|
1737
|
+
sage: H = IncidenceStructure(B) # needs sage.modules
|
1738
|
+
sage: H.is_generalized_quadrangle(verbose=True) # needs sage.modules
|
1739
|
+
Two blocks intersect on >1 points.
|
1740
|
+
False
|
1741
|
+
|
1742
|
+
sage: hypergraphs.CompleteUniform(4,2).is_generalized_quadrangle(verbose=1) # needs sage.modules
|
1743
|
+
Some point has two projections on some line.
|
1744
|
+
False
|
1745
|
+
"""
|
1746
|
+
# The distance between a point and a line in the incidence graph is odd
|
1747
|
+
# and must be <= 3. Thus, the diameter is at most 4
|
1748
|
+
g = self.incidence_graph()
|
1749
|
+
if g.diameter() > 4:
|
1750
|
+
if verbose:
|
1751
|
+
print("Some point is at distance >3 from some block.")
|
1752
|
+
return False
|
1753
|
+
|
1754
|
+
# There is a unique projection of a point on a line. Thus, the girth of
|
1755
|
+
# g is at least 7
|
1756
|
+
girth = g.girth()
|
1757
|
+
if girth == 4:
|
1758
|
+
if verbose:
|
1759
|
+
print("Two blocks intersect on >1 points.")
|
1760
|
+
return False
|
1761
|
+
elif girth == 6:
|
1762
|
+
if verbose:
|
1763
|
+
print("Some point has two projections on some line.")
|
1764
|
+
return False
|
1765
|
+
|
1766
|
+
if parameters:
|
1767
|
+
s = self.is_uniform()
|
1768
|
+
t = self.is_regular()
|
1769
|
+
s = s - 1 if (s is not False and s >= 2) else False
|
1770
|
+
t = t - 1 if (t is not False and t >= 2) else False
|
1771
|
+
return (s, t)
|
1772
|
+
else:
|
1773
|
+
return True
|
1774
|
+
|
1775
|
+
def dual(self, algorithm=None):
|
1776
|
+
"""
|
1777
|
+
Return the dual of the incidence structure.
|
1778
|
+
|
1779
|
+
INPUT:
|
1780
|
+
|
1781
|
+
- ``algorithm`` -- whether to use Sage's implementation
|
1782
|
+
(``algorithm=None``, default) or use GAP's (``algorithm='gap'``)
|
1783
|
+
|
1784
|
+
.. NOTE::
|
1785
|
+
|
1786
|
+
The ``algorithm='gap'`` option requires GAP's Design package
|
1787
|
+
(included in the ``gap_packages`` Sage spkg).
|
1788
|
+
|
1789
|
+
EXAMPLES:
|
1790
|
+
|
1791
|
+
The dual of a projective plane is a projective plane::
|
1792
|
+
|
1793
|
+
sage: PP = designs.DesarguesianProjectivePlaneDesign(4) # needs sage.rings.finite_rings
|
1794
|
+
sage: PP.dual().is_t_design(return_parameters=True) # needs sage.modules sage.rings.finite_rings
|
1795
|
+
(True, (2, 21, 5, 1))
|
1796
|
+
|
1797
|
+
TESTS::
|
1798
|
+
|
1799
|
+
sage: D = IncidenceStructure(4, [[0,2],[1,2,3],[2,3]]); D
|
1800
|
+
Incidence structure with 4 points and 3 blocks
|
1801
|
+
sage: D.dual() # needs sage.modules
|
1802
|
+
Incidence structure with 3 points and 4 blocks
|
1803
|
+
sage: print(D.dual(algorithm='gap')) # optional - gap_package_design
|
1804
|
+
Incidence structure with 3 points and 4 blocks
|
1805
|
+
sage: blocks = [[0,1,2],[0,3,4],[0,5,6],[1,3,5],[1,4,6],[2,3,6],[2,4,5]]
|
1806
|
+
sage: BD = IncidenceStructure(7, blocks, name='FanoPlane'); BD
|
1807
|
+
Incidence structure with 7 points and 7 blocks
|
1808
|
+
sage: print(BD.dual(algorithm='gap')) # optional - gap_package_design
|
1809
|
+
Incidence structure with 7 points and 7 blocks
|
1810
|
+
sage: BD.dual() # needs sage.modules
|
1811
|
+
Incidence structure with 7 points and 7 blocks
|
1812
|
+
|
1813
|
+
REFERENCE:
|
1814
|
+
|
1815
|
+
- Leonard Soicher, :gap_package:`Design package manual <design/htm/CHAP003.htm>`
|
1816
|
+
"""
|
1817
|
+
if algorithm == "gap":
|
1818
|
+
libgap.load_package("design")
|
1819
|
+
DD = libgap(self).DualBlockDesign()
|
1820
|
+
v = DD['v'].sage()
|
1821
|
+
gB = [[x - 1 for x in b] for b in DD['blocks'].sage()]
|
1822
|
+
return IncidenceStructure(list(range(v)), gB, name=None, check=False)
|
1823
|
+
|
1824
|
+
return IncidenceStructure(
|
1825
|
+
incidence_matrix=self.incidence_matrix().transpose(),
|
1826
|
+
check=False)
|
1827
|
+
|
1828
|
+
def automorphism_group(self):
|
1829
|
+
r"""
|
1830
|
+
Return the subgroup of the automorphism group of the incidence graph
|
1831
|
+
which respects the P B partition. It is (isomorphic to) the automorphism
|
1832
|
+
group of the block design, although the degrees differ.
|
1833
|
+
|
1834
|
+
EXAMPLES::
|
1835
|
+
|
1836
|
+
sage: # needs sage.groups sage.rings.finite_rings
|
1837
|
+
sage: P = designs.DesarguesianProjectivePlaneDesign(2); P
|
1838
|
+
(7,3,1)-Balanced Incomplete Block Design
|
1839
|
+
sage: G = P.automorphism_group()
|
1840
|
+
sage: G.is_isomorphic(PGL(3,2))
|
1841
|
+
True
|
1842
|
+
sage: G
|
1843
|
+
Permutation Group with generators [...]
|
1844
|
+
sage: G.cardinality()
|
1845
|
+
168
|
1846
|
+
|
1847
|
+
A non self-dual example::
|
1848
|
+
|
1849
|
+
sage: IS = IncidenceStructure(list(range(4)), [[0,1,2,3],[1,2,3]])
|
1850
|
+
sage: IS.automorphism_group().cardinality() # needs sage.groups
|
1851
|
+
6
|
1852
|
+
sage: IS.dual().automorphism_group().cardinality() # needs sage.groups sage.modules
|
1853
|
+
1
|
1854
|
+
|
1855
|
+
Examples with non-integer points::
|
1856
|
+
|
1857
|
+
sage: I = IncidenceStructure('abc', ('ab','ac','bc'))
|
1858
|
+
sage: I.automorphism_group() # needs sage.groups
|
1859
|
+
Permutation Group with generators [('b','c'), ('a','b')]
|
1860
|
+
sage: IncidenceStructure([[(1,2),(3,4)]]).automorphism_group() # needs sage.groups
|
1861
|
+
Permutation Group with generators [((1,2),(3,4))]
|
1862
|
+
"""
|
1863
|
+
from sage.graphs.graph import Graph
|
1864
|
+
from sage.groups.perm_gps.permgroup import PermutationGroup
|
1865
|
+
g = Graph()
|
1866
|
+
n = self.num_points()
|
1867
|
+
g.add_edges((i + n, x) for i, b in enumerate(self._blocks) for x in b)
|
1868
|
+
ag = g.automorphism_group(partition=[list(range(n)),
|
1869
|
+
list(range(n, n + self.num_blocks()))])
|
1870
|
+
|
1871
|
+
if self._point_to_index:
|
1872
|
+
gens = [[tuple([self._points[i] for i in cycle if (not cycle or cycle[0] < n)])
|
1873
|
+
for cycle in g.cycle_tuples()]
|
1874
|
+
for g in ag.gens()]
|
1875
|
+
else:
|
1876
|
+
gens = [[tuple(cycle) for cycle in g.cycle_tuples() if (not cycle or cycle[0] < n)]
|
1877
|
+
for g in ag.gens()]
|
1878
|
+
|
1879
|
+
return PermutationGroup(gens, domain=self._points)
|
1880
|
+
|
1881
|
+
def is_resolvable(self, certificate=False, solver=None, verbose=0, check=True,
|
1882
|
+
*, integrality_tolerance=1e-3):
|
1883
|
+
r"""
|
1884
|
+
Test whether the hypergraph is resolvable.
|
1885
|
+
|
1886
|
+
A hypergraph is said to be resolvable if its sets can be partitionned
|
1887
|
+
into classes, each of which is a partition of the ground set.
|
1888
|
+
|
1889
|
+
.. NOTE::
|
1890
|
+
|
1891
|
+
This problem is solved using an Integer Linear Program, and GLPK
|
1892
|
+
(the default LP solver) has been reported to be very slow on some
|
1893
|
+
instances. If you hit this wall, consider installing a more powerful
|
1894
|
+
MILP solver (CPLEX, Gurobi, ...).
|
1895
|
+
|
1896
|
+
INPUT:
|
1897
|
+
|
1898
|
+
- ``certificate`` -- boolean; whether to return the classes along with
|
1899
|
+
the binary answer (see examples below)
|
1900
|
+
|
1901
|
+
- ``solver`` -- (default: ``None``) specify a Mixed Integer Linear
|
1902
|
+
Programming (MILP) solver to be used. If set to ``None``, the default
|
1903
|
+
one is used. For more information on MILP solvers and which default
|
1904
|
+
solver is used, see the method :meth:`solve
|
1905
|
+
<sage.numerical.mip.MixedIntegerLinearProgram.solve>` of the class
|
1906
|
+
:class:`MixedIntegerLinearProgram
|
1907
|
+
<sage.numerical.mip.MixedIntegerLinearProgram>`.
|
1908
|
+
|
1909
|
+
- ``verbose`` -- integer (default: 0); sets the level of
|
1910
|
+
verbosity. Set to 0 by default, which means quiet.
|
1911
|
+
|
1912
|
+
- ``check`` -- boolean (default: ``True``); whether to check that
|
1913
|
+
output is correct before returning it. As this is expected to be
|
1914
|
+
useless, you may want to disable it whenever you want speed.
|
1915
|
+
|
1916
|
+
- ``integrality_tolerance`` -- parameter for use with MILP solvers over
|
1917
|
+
an inexact base ring; see :meth:`MixedIntegerLinearProgram.get_values`
|
1918
|
+
|
1919
|
+
EXAMPLES:
|
1920
|
+
|
1921
|
+
Some resolvable designs::
|
1922
|
+
|
1923
|
+
sage: TD = designs.transversal_design(2,2,resolvable=True)
|
1924
|
+
sage: TD.is_resolvable()
|
1925
|
+
True
|
1926
|
+
|
1927
|
+
sage: AG = designs.AffineGeometryDesign(3,1,GF(2)) # needs sage.combinat sage.modules
|
1928
|
+
sage: AG.is_resolvable() # needs sage.combinat sage.modules
|
1929
|
+
True
|
1930
|
+
|
1931
|
+
Their classes::
|
1932
|
+
|
1933
|
+
sage: b, cls = TD.is_resolvable(True)
|
1934
|
+
sage: b
|
1935
|
+
True
|
1936
|
+
sage: cls # random
|
1937
|
+
[[[0, 3], [1, 2]], [[1, 3], [0, 2]]]
|
1938
|
+
|
1939
|
+
sage: # needs sage.combinat
|
1940
|
+
sage: b, cls = AG.is_resolvable(True)
|
1941
|
+
sage: b
|
1942
|
+
True
|
1943
|
+
sage: cls # random
|
1944
|
+
[[[6, 7], [4, 5], [0, 1], [2, 3]],
|
1945
|
+
[[5, 7], [0, 4], [3, 6], [1, 2]],
|
1946
|
+
[[0, 2], [4, 7], [1, 3], [5, 6]],
|
1947
|
+
[[3, 4], [0, 7], [1, 5], [2, 6]],
|
1948
|
+
[[3, 7], [1, 6], [0, 5], [2, 4]],
|
1949
|
+
[[0, 6], [2, 7], [1, 4], [3, 5]],
|
1950
|
+
[[4, 6], [0, 3], [2, 5], [1, 7]]]
|
1951
|
+
|
1952
|
+
A non-resolvable design::
|
1953
|
+
|
1954
|
+
sage: Fano = designs.balanced_incomplete_block_design(7,3) # needs sage.schemes
|
1955
|
+
sage: Fano.is_resolvable() # needs sage.schemes
|
1956
|
+
False
|
1957
|
+
sage: Fano.is_resolvable(True) # needs sage.schemes
|
1958
|
+
(False, [])
|
1959
|
+
|
1960
|
+
TESTS::
|
1961
|
+
|
1962
|
+
sage: # needs sage.combinat
|
1963
|
+
sage: _, cls1 = AG.is_resolvable(certificate=True)
|
1964
|
+
sage: _, cls2 = AG.is_resolvable(certificate=True)
|
1965
|
+
sage: cls1 is cls2
|
1966
|
+
False
|
1967
|
+
"""
|
1968
|
+
if self._classes is None:
|
1969
|
+
degrees = set(self.degrees().values())
|
1970
|
+
if len(degrees) != 1:
|
1971
|
+
self._classes = False
|
1972
|
+
else:
|
1973
|
+
from sage.numerical.mip import MixedIntegerLinearProgram
|
1974
|
+
from sage.numerical.mip import MIPSolverException
|
1975
|
+
n_classes = degrees.pop()
|
1976
|
+
p = MixedIntegerLinearProgram(solver=solver)
|
1977
|
+
b = p.new_variable(binary=True)
|
1978
|
+
domain = list(range(self.num_points()))
|
1979
|
+
|
1980
|
+
# Lists of blocks containing i for every i
|
1981
|
+
dual = [[] for _ in domain]
|
1982
|
+
for i, B in enumerate(self._blocks):
|
1983
|
+
for x in B:
|
1984
|
+
dual[x].append(i)
|
1985
|
+
|
1986
|
+
# Each class is a partition
|
1987
|
+
for t in range(n_classes):
|
1988
|
+
for x in domain:
|
1989
|
+
p.add_constraint(p.sum(b[t, i] for i in dual[x]) == 1)
|
1990
|
+
|
1991
|
+
# Each set appears exactly once
|
1992
|
+
for i in range(len(self._blocks)):
|
1993
|
+
p.add_constraint(p.sum(b[t, i] for t in range(n_classes)) == 1)
|
1994
|
+
|
1995
|
+
try:
|
1996
|
+
p.solve(log=verbose)
|
1997
|
+
except MIPSolverException:
|
1998
|
+
self._classes = False
|
1999
|
+
else:
|
2000
|
+
# each class is stored as the list of indices of its blocks
|
2001
|
+
self._classes = [[] for _ in range(n_classes)]
|
2002
|
+
for (t, i), v in p.get_values(b, convert=bool, tolerance=integrality_tolerance).items():
|
2003
|
+
if v:
|
2004
|
+
self._classes[t].append(self._blocks[i])
|
2005
|
+
|
2006
|
+
if check and self._classes is not False:
|
2007
|
+
assert sorted(id(c) for cls in self._classes for c in cls) == sorted(id(b) for b in self._blocks), "some set does not appear exactly once"
|
2008
|
+
domain = list(range(self.num_points()))
|
2009
|
+
for i, c in enumerate(self._classes):
|
2010
|
+
assert sorted(sum(c, [])) == domain, "class {} is not a partition".format(i)
|
2011
|
+
|
2012
|
+
if self._classes is False:
|
2013
|
+
return (False, []) if certificate else False
|
2014
|
+
|
2015
|
+
if certificate:
|
2016
|
+
if self._point_to_index is None:
|
2017
|
+
classes = [[block[:] for block in classs] for classs in self._classes]
|
2018
|
+
else:
|
2019
|
+
classes = [[[self._points[i] for i in block] for block in classs] for classs in self._classes]
|
2020
|
+
|
2021
|
+
return (True, classes)
|
2022
|
+
|
2023
|
+
else:
|
2024
|
+
return True
|
2025
|
+
|
2026
|
+
def coloring(self, k=None, solver=None, verbose=0,
|
2027
|
+
*, integrality_tolerance=1e-3):
|
2028
|
+
r"""
|
2029
|
+
Compute a (weak) `k`-coloring of the hypergraph.
|
2030
|
+
|
2031
|
+
A weak coloring of a hypergraph `\mathcal H` is an assignment of colors
|
2032
|
+
to its vertices such that no set is monochromatic.
|
2033
|
+
|
2034
|
+
INPUT:
|
2035
|
+
|
2036
|
+
- ``k`` -- integer; compute a coloring with `k` colors if an integer is
|
2037
|
+
provided, otherwise returns an optimal coloring (i.e. with the minimum
|
2038
|
+
possible number of colors).
|
2039
|
+
|
2040
|
+
- ``solver`` -- (default: ``None``) specify a Mixed Integer Linear
|
2041
|
+
Programming (MILP) solver to be used. If set to ``None``, the default
|
2042
|
+
one is used. For more information on MILP solvers and which default
|
2043
|
+
solver is used, see the method :meth:`solve
|
2044
|
+
<sage.numerical.mip.MixedIntegerLinearProgram.solve>` of the class
|
2045
|
+
:class:`MixedIntegerLinearProgram
|
2046
|
+
<sage.numerical.mip.MixedIntegerLinearProgram>`.
|
2047
|
+
|
2048
|
+
- ``verbose`` -- nonnegative integer (default: `0`); set the level
|
2049
|
+
of verbosity you want from the linear program solver. Since the
|
2050
|
+
problem is `NP`-complete, its solving may take some time depending on
|
2051
|
+
the graph. A value of `0` means that there will be no message printed by
|
2052
|
+
the solver.
|
2053
|
+
|
2054
|
+
- ``integrality_tolerance`` -- parameter for use with MILP solvers over
|
2055
|
+
an inexact base ring; see :meth:`MixedIntegerLinearProgram.get_values`
|
2056
|
+
|
2057
|
+
EXAMPLES:
|
2058
|
+
|
2059
|
+
The Fano plane has chromatic number 3::
|
2060
|
+
|
2061
|
+
sage: len(designs.steiner_triple_system(7).coloring()) # needs sage.numerical.mip
|
2062
|
+
3
|
2063
|
+
|
2064
|
+
One admissible 3-coloring::
|
2065
|
+
|
2066
|
+
sage: designs.steiner_triple_system(7).coloring() # not tested # needs sage.numerical.mip
|
2067
|
+
[[0, 2, 5, 1], [4, 3], [6]]
|
2068
|
+
|
2069
|
+
The chromatic number of a graph is equal to the chromatic number of its
|
2070
|
+
2-uniform corresponding hypergraph::
|
2071
|
+
|
2072
|
+
sage: g = graphs.PetersenGraph()
|
2073
|
+
sage: H = IncidenceStructure(g.edges(sort=True, labels=False))
|
2074
|
+
sage: len(g.coloring())
|
2075
|
+
3
|
2076
|
+
sage: len(H.coloring()) # needs sage.numerical.mip
|
2077
|
+
3
|
2078
|
+
"""
|
2079
|
+
if k is None:
|
2080
|
+
for k in range(self.num_points() + 1):
|
2081
|
+
try:
|
2082
|
+
return self.coloring(k)
|
2083
|
+
except ValueError:
|
2084
|
+
pass
|
2085
|
+
|
2086
|
+
if k == 0:
|
2087
|
+
if self.num_points():
|
2088
|
+
raise ValueError("Only empty hypergraphs are 0-chromatic")
|
2089
|
+
return []
|
2090
|
+
elif any(len(x) == 1 for x in self._blocks):
|
2091
|
+
raise RuntimeError("No coloring can be defined "
|
2092
|
+
"when there is a set of size 1")
|
2093
|
+
elif k == 1:
|
2094
|
+
if any(self._blocks):
|
2095
|
+
raise ValueError("This hypergraph contains a set. "
|
2096
|
+
"It is not 1-chromatic")
|
2097
|
+
return [self.ground_set()]
|
2098
|
+
|
2099
|
+
from sage.numerical.mip import MixedIntegerLinearProgram, MIPSolverException
|
2100
|
+
p = MixedIntegerLinearProgram(solver=solver)
|
2101
|
+
b = p.new_variable(binary=True)
|
2102
|
+
|
2103
|
+
for x in range(self.num_points()):
|
2104
|
+
p.add_constraint(p.sum(b[x, i] for i in range(k)) == 1)
|
2105
|
+
|
2106
|
+
for s in self._blocks:
|
2107
|
+
for i in range(k):
|
2108
|
+
p.add_constraint(p.sum(b[x, i] for x in s) <= len(s) - 1)
|
2109
|
+
|
2110
|
+
try:
|
2111
|
+
p.solve(log=verbose)
|
2112
|
+
except MIPSolverException:
|
2113
|
+
raise ValueError("This hypergraph is not {}-colorable".format(k))
|
2114
|
+
|
2115
|
+
col = [[] for _ in range(k)]
|
2116
|
+
|
2117
|
+
for (x, i), v in p.get_values(b, convert=bool, tolerance=integrality_tolerance).items():
|
2118
|
+
if v:
|
2119
|
+
col[i].append(self._points[x])
|
2120
|
+
|
2121
|
+
return col
|
2122
|
+
|
2123
|
+
def edge_coloring(self) -> list:
|
2124
|
+
r"""
|
2125
|
+
Compute a proper edge-coloring.
|
2126
|
+
|
2127
|
+
A proper edge-coloring is an assignment of colors to the sets of the
|
2128
|
+
incidence structure such that two sets with non-empty intersection
|
2129
|
+
receive different colors. The coloring returned minimizes the number of
|
2130
|
+
colors.
|
2131
|
+
|
2132
|
+
OUTPUT: a partition of the sets into color classes
|
2133
|
+
|
2134
|
+
EXAMPLES::
|
2135
|
+
|
2136
|
+
sage: # needs cliquer
|
2137
|
+
sage: H = Hypergraph([{1,2,3},{2,3,4},{3,4,5},{4,5,6}]); H
|
2138
|
+
Incidence structure with 6 points and 4 blocks
|
2139
|
+
sage: C = H.edge_coloring()
|
2140
|
+
sage: C # random
|
2141
|
+
[[[3, 4, 5]], [[2, 3, 4]], [[4, 5, 6], [1, 2, 3]]]
|
2142
|
+
sage: Set(map(Set,sum(C,[]))) == Set(map(Set,H.blocks()))
|
2143
|
+
True
|
2144
|
+
"""
|
2145
|
+
from sage.graphs.graph import Graph
|
2146
|
+
blocks = self.blocks()
|
2147
|
+
blocks_sets = [frozenset(b) for b in blocks]
|
2148
|
+
g = Graph([list(range(self.num_blocks())),
|
2149
|
+
lambda x, y: len(blocks_sets[x] & blocks_sets[y])],
|
2150
|
+
loops=False)
|
2151
|
+
return [[blocks[i] for i in C] for C in g.coloring(algorithm='MILP')]
|
2152
|
+
|
2153
|
+
def _spring_layout(self):
|
2154
|
+
r"""
|
2155
|
+
Return a spring layout for the points.
|
2156
|
+
|
2157
|
+
The layout is computed by creating a graph `G` on the points *and* sets
|
2158
|
+
of the incidence structure. Each set is then made adjacent in `G` with
|
2159
|
+
all points it contains before a spring layout is computed for this
|
2160
|
+
graph. The position of the points in the graph gives the position of the
|
2161
|
+
points in the final drawing.
|
2162
|
+
|
2163
|
+
.. NOTE::
|
2164
|
+
|
2165
|
+
This method also returns the position of the "fake" points,
|
2166
|
+
i.e. those representing the sets.
|
2167
|
+
|
2168
|
+
EXAMPLES::
|
2169
|
+
|
2170
|
+
sage: # needs sage.plot
|
2171
|
+
sage: H = Hypergraph([{1,2,3},{2,3,4},{3,4,5},{4,5,6}]); H
|
2172
|
+
Incidence structure with 6 points and 4 blocks
|
2173
|
+
sage: L = H._spring_layout()
|
2174
|
+
sage: L # random
|
2175
|
+
{1: (0.238, -0.926),
|
2176
|
+
2: (0.672, -0.518),
|
2177
|
+
3: (0.449, -0.225),
|
2178
|
+
4: (0.782, 0.225),
|
2179
|
+
5: (0.558, 0.518),
|
2180
|
+
6: (0.992, 0.926),
|
2181
|
+
{3, 4, 5}: (0.504, 0.173),
|
2182
|
+
{2, 3, 4}: (0.727, -0.173),
|
2183
|
+
{4, 5, 6}: (0.838, 0.617),
|
2184
|
+
{1, 2, 3}: (0.393, -0.617)}
|
2185
|
+
sage: all(v in L for v in H.ground_set())
|
2186
|
+
True
|
2187
|
+
sage: all(v in L for v in map(Set, H.blocks()))
|
2188
|
+
True
|
2189
|
+
"""
|
2190
|
+
from sage.graphs.graph import Graph
|
2191
|
+
|
2192
|
+
g = Graph()
|
2193
|
+
for s in map(Set, self.blocks()):
|
2194
|
+
for x in s:
|
2195
|
+
g.add_edge((0, s), (1, x))
|
2196
|
+
|
2197
|
+
_ = g.plot(iterations=50000, save_pos=True)
|
2198
|
+
|
2199
|
+
# The values are rounded as TikZ does not like accuracy.
|
2200
|
+
return {k[1]: (round(x, 3), round(y, 3))
|
2201
|
+
for k, (x, y) in g.get_pos().items()}
|
2202
|
+
|
2203
|
+
def _latex_(self) -> str:
|
2204
|
+
r"""
|
2205
|
+
Return a TikZ representation of the incidence structure.
|
2206
|
+
|
2207
|
+
EXAMPLES::
|
2208
|
+
|
2209
|
+
sage: H = Hypergraph([{1,2,3},{2,3,4},{3,4,5},{4,5,6}]); H
|
2210
|
+
Incidence structure with 6 points and 4 blocks
|
2211
|
+
sage: view(H) # not tested
|
2212
|
+
|
2213
|
+
With sets of size 4::
|
2214
|
+
|
2215
|
+
sage: g = graphs.Grid2dGraph(5,5)
|
2216
|
+
sage: C4 = graphs.CycleGraph(4)
|
2217
|
+
sage: sets = Set(map(Set, g.subgraph_search_iterator(C4, # needs sage.modules
|
2218
|
+
....: return_graphs=False)))
|
2219
|
+
sage: H = Hypergraph(sets) # needs sage.modules
|
2220
|
+
sage: view(H) # not tested # needs sage.modules sage.plot
|
2221
|
+
|
2222
|
+
TESTS::
|
2223
|
+
|
2224
|
+
# verify that :issue:`30976` is fixed
|
2225
|
+
sage: IS = IncidenceStructure([1,2,3], [[1,2], [2,3]])
|
2226
|
+
sage: if latex.has_file("tikz.sty"): # optional - latex
|
2227
|
+
....: IS._latex_()
|
2228
|
+
...UserWarning:
|
2229
|
+
The hypergraph is drawn as a set of closed curves...
|
2230
|
+
\begin{tikzpicture}...
|
2231
|
+
\draw... -- ...;
|
2232
|
+
\draw... -- ...;
|
2233
|
+
\draw node...;
|
2234
|
+
\draw node...;
|
2235
|
+
\draw node...;
|
2236
|
+
\end{tikzpicture}
|
2237
|
+
"""
|
2238
|
+
from sage.functions.trig import arctan2
|
2239
|
+
|
2240
|
+
from warnings import warn
|
2241
|
+
warn("\nThe hypergraph is drawn as a set of closed curves. The curve "
|
2242
|
+
"representing a set S goes **THROUGH** the points contained "
|
2243
|
+
"in S.\n A point which is encircled by a curve but is not located "
|
2244
|
+
"on its boundary is **NOT** included in the corresponding set.\n"
|
2245
|
+
"\n"
|
2246
|
+
"The colors are picked for readability and have no other meaning.")
|
2247
|
+
|
2248
|
+
latex.add_package_to_preamble_if_available("tikz")
|
2249
|
+
|
2250
|
+
if not latex.has_file("tikz.sty"):
|
2251
|
+
raise RuntimeError("You must have TikZ installed in order "
|
2252
|
+
"to draw a hypergraph.")
|
2253
|
+
|
2254
|
+
domain = self.ground_set()
|
2255
|
+
pos = self._spring_layout()
|
2256
|
+
tex = "\\begin{tikzpicture}[scale=3]\n"
|
2257
|
+
|
2258
|
+
colors = ["black", "red", "green", "blue", "cyan",
|
2259
|
+
"magenta", "yellow", "pink", "brown"]
|
2260
|
+
colored_sets = [(s, i) for i, S in enumerate(self.edge_coloring()) for s in S]
|
2261
|
+
|
2262
|
+
# Prints each set with its color
|
2263
|
+
for s, i in colored_sets:
|
2264
|
+
current_color = colors[i % len(colors)]
|
2265
|
+
|
2266
|
+
if len(s) == 2:
|
2267
|
+
s = list(s)
|
2268
|
+
tex += ("\\draw[color="+str(current_color)+"," +
|
2269
|
+
"line width=.1cm,opacity = .6] " +
|
2270
|
+
str(pos[s[0]])+" -- "+str(pos[s[1]])+";\n")
|
2271
|
+
continue
|
2272
|
+
|
2273
|
+
tex += ("\\draw[color="+str(current_color)+","
|
2274
|
+
"line width=.1cm,opacity = .6,"
|
2275
|
+
"line cap=round,"
|
2276
|
+
"line join=round]"
|
2277
|
+
"plot [smooth cycle,tension=1] coordinates {")
|
2278
|
+
|
2279
|
+
# Reorders the vertices of s according to their angle with the
|
2280
|
+
# "center", i.e. the vertex representing the set s
|
2281
|
+
cx, cy = pos[Set(s)]
|
2282
|
+
s = [pos[_] for _ in s]
|
2283
|
+
s = sorted(s, key=lambda x_y: arctan2(x_y[0] - cx, x_y[1] - cy))
|
2284
|
+
|
2285
|
+
for x in s:
|
2286
|
+
tex += str(x)+" "
|
2287
|
+
tex += "};\n"
|
2288
|
+
|
2289
|
+
# Prints each vertex
|
2290
|
+
for v in domain:
|
2291
|
+
tex += "\\draw node[fill,circle,scale=.5,label={90:$"+latex(v)+"$}] at "+str(pos[v])+" {};\n"
|
2292
|
+
|
2293
|
+
tex += "\\end{tikzpicture}"
|
2294
|
+
return tex
|
2295
|
+
|
2296
|
+
def is_spread(self, spread) -> bool:
|
2297
|
+
r"""
|
2298
|
+
Check whether the input is a spread for ``self``.
|
2299
|
+
|
2300
|
+
A spread of an incidence structure `(P, B)` is a subset of `B` which
|
2301
|
+
forms a partition of `P`.
|
2302
|
+
|
2303
|
+
INPUT:
|
2304
|
+
|
2305
|
+
- ``spread`` -- iterable; defines the spread
|
2306
|
+
|
2307
|
+
EXAMPLES::
|
2308
|
+
|
2309
|
+
sage: E = IncidenceStructure([[1, 2, 3], [4, 5, 6], [1, 5, 6]])
|
2310
|
+
sage: E.is_spread([[1, 2, 3], [4, 5, 6]])
|
2311
|
+
True
|
2312
|
+
sage: E.is_spread([1, 2, 3, 4, 5, 6])
|
2313
|
+
Traceback (most recent call last):
|
2314
|
+
...
|
2315
|
+
TypeError: 'sage.rings.integer.Integer' object is not iterable
|
2316
|
+
sage: E.is_spread([[1, 2, 3, 4], [5, 6]])
|
2317
|
+
False
|
2318
|
+
|
2319
|
+
Order of blocks or of points within each block doesn't matter::
|
2320
|
+
|
2321
|
+
sage: E = IncidenceStructure([[1, 2, 3], [4, 5, 6], [1, 5, 6]])
|
2322
|
+
sage: E.is_spread([[5, 6, 4], [3, 1, 2]])
|
2323
|
+
True
|
2324
|
+
|
2325
|
+
TESTS::
|
2326
|
+
|
2327
|
+
sage: E = IncidenceStructure([])
|
2328
|
+
sage: E.is_spread([])
|
2329
|
+
True
|
2330
|
+
sage: E = IncidenceStructure([[1]])
|
2331
|
+
sage: E.is_spread([])
|
2332
|
+
False
|
2333
|
+
sage: E.is_spread([[1]])
|
2334
|
+
True
|
2335
|
+
sage: E = IncidenceStructure([[1], [1]])
|
2336
|
+
sage: E.is_spread([[1]])
|
2337
|
+
True
|
2338
|
+
"""
|
2339
|
+
|
2340
|
+
points = set(self.ground_set())
|
2341
|
+
allBlocks = set(map(frozenset, self.blocks()))
|
2342
|
+
for block in spread:
|
2343
|
+
sblock = set(block)
|
2344
|
+
|
2345
|
+
if sblock not in allBlocks:
|
2346
|
+
return False
|
2347
|
+
|
2348
|
+
if not points.issuperset(sblock):
|
2349
|
+
return False
|
2350
|
+
|
2351
|
+
points.difference_update(sblock)
|
2352
|
+
|
2353
|
+
return not points
|
2354
|
+
|
2355
|
+
|
2356
|
+
from sage.misc.rest_index_of_methods import gen_rest_table_index
|
2357
|
+
__doc__ = __doc__.format(METHODS_OF_IncidenceStructure=gen_rest_table_index(IncidenceStructure))
|