diff --git a/docs/source/conf.py b/docs/source/conf.py index bee37bf0..0cb6c75a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -179,6 +179,7 @@ def no_doc_run(self): ("Transf", "Perm"), ], "FroidurePinPBR": [(r"\bPBR\b", "Element")], + "SchreierSimsPerm1": [(r"\bPerm1\b", "Element")], "ReversiblePaths": [(r"\bPaths\b", "ReversiblePaths")], } diff --git a/docs/source/index.rst b/docs/source/index.rst index ed7f27cf..2581ae45 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -71,6 +71,7 @@ See the installation instructions: main-algorithms/knuth-bendix/index main-algorithms/konieczny/index main-algorithms/radoszewski-rytter/index + main-algorithms/schreier-sims/index main-algorithms/stephen/index main-algorithms/todd-coxeter/index main-algorithms/ukkonen/index diff --git a/docs/source/main-algorithms/schreier-sims/index.rst b/docs/source/main-algorithms/schreier-sims/index.rst new file mode 100644 index 00000000..9c05e525 --- /dev/null +++ b/docs/source/main-algorithms/schreier-sims/index.rst @@ -0,0 +1,19 @@ +.. Copyright (c) 2024 Joseph Edwards + + Distributed under the terms of the GPL license version 3. + + The full license is in the file LICENSE, distributed with this software. + +Schreier-Sims +============= + +This page describes the functionality related to the Schreier-Sims algorithm for +computing a stabilizer chain of a permutation group in +``libsemigroups_pybind11``. + + +.. toctree:: + :maxdepth: 1 + + schreier-sims + schreier-sims-helpers diff --git a/docs/source/main-algorithms/schreier-sims/schreier-sims-helpers.rst b/docs/source/main-algorithms/schreier-sims/schreier-sims-helpers.rst new file mode 100644 index 00000000..528fd715 --- /dev/null +++ b/docs/source/main-algorithms/schreier-sims/schreier-sims-helpers.rst @@ -0,0 +1,33 @@ +.. Copyright (c) 2024 Joseph Edwards + + Distributed under the terms of the GPL license version 3. + + The full license is in the file LICENSE, distributed with this software. + +Schreier-Sims helper functions +============================== + +This page contains the documentation for various helper functions for +manipulating :any:`SchreierSimsPerm1` objects. + +Contents +-------- + +In ``libsemigroups_pybind11``: + +.. currentmodule:: libsemigroups_pybind11.schreier_sims + +.. autosummary:: + :nosignatures: + + intersection + + +Full API +-------- + +.. currentmodule:: libsemigroups_pybind11 + +.. automodule:: libsemigroups_pybind11.schreier_sims + :members: + :imported-members: diff --git a/docs/source/main-algorithms/schreier-sims/schreier-sims.rst b/docs/source/main-algorithms/schreier-sims/schreier-sims.rst new file mode 100644 index 00000000..888fc72e --- /dev/null +++ b/docs/source/main-algorithms/schreier-sims/schreier-sims.rst @@ -0,0 +1,51 @@ +.. Copyright (c) 2023-2024 J. D. Mitchell + + Distributed under the terms of the GPL license version 3. + + The full license is in the file LICENSE, distributed with this software. + +.. currentmodule:: _libsemigroups_pybind11 + +The Schreier-Sims class +======================= + +.. autoclass:: SchreierSimsPerm1 + :doc-only: + :class-doc-from: class + +Contents +-------- + +.. autosummary:: + :nosignatures: + + SchreierSimsPerm1.add_base_point + SchreierSimsPerm1.add_generator + SchreierSimsPerm1.base + SchreierSimsPerm1.base_size + SchreierSimsPerm1.contains + SchreierSimsPerm1.current_size + SchreierSimsPerm1.currently_contains + SchreierSimsPerm1.empty + SchreierSimsPerm1.finished + SchreierSimsPerm1.generator + SchreierSimsPerm1.init + SchreierSimsPerm1.inverse_transversal_element + SchreierSimsPerm1.number_of_generators + SchreierSimsPerm1.number_of_strong_generators + SchreierSimsPerm1.one + SchreierSimsPerm1.orbit_lookup + SchreierSimsPerm1.run + SchreierSimsPerm1.sift + SchreierSimsPerm1.sift_inplace + SchreierSimsPerm1.size + SchreierSimsPerm1.strong_generator + SchreierSimsPerm1.transversal_element + +Full API +-------- + +.. autoclass:: SchreierSimsPerm1 + :members: + :class-doc-from: init + diff --git a/etc/replace-strings-in-doc.py b/etc/replace-strings-in-doc.py index 07d9d89e..efde2e5f 100755 --- a/etc/replace-strings-in-doc.py +++ b/etc/replace-strings-in-doc.py @@ -54,6 +54,7 @@ def dive(path): "ImageRightActionPPerm1PPerm1": "ImageRightAction", "_libsemigroups_pybind11.FroidurePinBase": "FroidurePinBase", "FroidurePinPBR": "FroidurePin", + "SchreierSimsPerm1": "SchreierSims", } files = all_html_files(html_path) diff --git a/libsemigroups_pybind11/__init__.py b/libsemigroups_pybind11/__init__.py index ff667c93..414ba8d5 100644 --- a/libsemigroups_pybind11/__init__.py +++ b/libsemigroups_pybind11/__init__.py @@ -103,3 +103,4 @@ MatrixKind.__name__ = "MatrixKind" from .froidure_pin import FroidurePin +from .schreier_sims import SchreierSims diff --git a/libsemigroups_pybind11/action.py b/libsemigroups_pybind11/action.py index b06712be..5323b2d8 100644 --- a/libsemigroups_pybind11/action.py +++ b/libsemigroups_pybind11/action.py @@ -56,7 +56,7 @@ class Action(Runner): # pylint: disable=invalid-name, too-many-instance-attribu src/action.cpp! """ - py_to_cxx_type_dict = { + _py_to_cxx_type_dict = { (BMat8, BMat8, ImageRightAction, side.right): _RightActionBMat8BMat8, (BMat8, BMat8, ImageLeftAction, side.left): _LeftActionBMat8BMat8, (PPerm, PPerm, ImageRightAction, side.right): { diff --git a/libsemigroups_pybind11/adapters.py b/libsemigroups_pybind11/adapters.py index f44a9dcf..5a195847 100644 --- a/libsemigroups_pybind11/adapters.py +++ b/libsemigroups_pybind11/adapters.py @@ -41,9 +41,9 @@ def __init__(self: Self, **kwargs): super().__init__(("Element", "Point"), **kwargs) def _init_cxx_obj(self: Self, elt: Any, pt: Any) -> Any: - cpp_obj_t = self._cxx_obj_type_from(samples=(elt, pt)) - if self._cxx_obj is None or not isinstance(self._cxx_obj, cpp_obj_t): - self._cxx_obj = cpp_obj_t() + cxx_obj_t = self._cxx_obj_type_from(samples=(elt, pt)) + if self._cxx_obj is None or not isinstance(self._cxx_obj, cxx_obj_t): + self._cxx_obj = cxx_obj_t() return self._cxx_obj def __call__( # pylint: disable=inconsistent-return-statements @@ -88,7 +88,7 @@ class ImageRightAction(_ImageAction): * *Point* -- the type of the points acted on """ - py_to_cxx_type_dict = { + _py_to_cxx_type_dict = { (_BMat8, _BMat8): _ImageRightActionBMat8BMat8, (PPerm, PPerm): { (_PPerm1, _PPerm1): _ImageRightActionPPerm1PPerm1, @@ -109,7 +109,7 @@ class ImageLeftAction(_ImageAction): # pylint: disable=invalid-name * *Point* -- the type of the points acted on """ - py_to_cxx_type_dict = { + _py_to_cxx_type_dict = { (_BMat8, _BMat8): _ImageLeftActionBMat8BMat8, (PPerm, PPerm): { (_PPerm1, _PPerm1): _ImageLeftActionPPerm1PPerm1, diff --git a/libsemigroups_pybind11/detail/cxx_wrapper.py b/libsemigroups_pybind11/detail/cxx_wrapper.py index 9947df73..06187460 100644 --- a/libsemigroups_pybind11/detail/cxx_wrapper.py +++ b/libsemigroups_pybind11/detail/cxx_wrapper.py @@ -65,7 +65,7 @@ def __init__(self: Self, expected_kwargs, **kwargs): # the next line ensures we get the values in the same order as in # lookup values = tuple(kwargs[x] for x in expected_kwargs) - lookup = self.py_to_cxx_type_dict + lookup = self._py_to_cxx_type_dict if values in lookup: for key, val in kwargs.items(): setattr(self, key, val) @@ -103,19 +103,37 @@ def __repr__(self: Self) -> str: return self._cxx_obj.__repr__() return "" + def __copy__(self: Self) -> str: + if self._cxx_obj is not None: + if hasattr(self._cxx_obj, "__copy__"): + return self._cxx_obj.__copy__() + raise NotImplementedError( + f"{type(self._cxx_obj)} has no member named __copy__" + ) + raise NameError("_cxx_obj has not been defined") + + def __eq__(self: Self, that) -> bool: + if self._cxx_obj is not None: + if hasattr(self._cxx_obj, "__eq__"): + return self._cxx_obj.__eq__(that) + raise NotImplementedError( + f"{type(self._cxx_obj)} has no member named __eq__" + ) + raise NameError("_cxx_obj has not been defined") + @property - def py_to_cxx_type_dict(self: Self) -> dict: + def _py_to_cxx_type_dict(self: Self) -> dict: return self.__class__.__lookup # TODO type annotations - @py_to_cxx_type_dict.setter - def py_to_cxx_type_dict(self: Self, value): + @_py_to_cxx_type_dict.setter + def _py_to_cxx_type_dict(self: Self, value): # TODO check that value is a dict of the correct structure self.__class__.__lookup = value def _cxx_obj_type_from(self: Self, samples=(), types=()) -> Any: py_types = tuple([type(x) for x in samples] + list(types)) - lookup = self.py_to_cxx_type_dict + lookup = self._py_to_cxx_type_dict if py_types not in lookup: raise ValueError( f"unexpected keyword argument combination {py_types}, " @@ -124,10 +142,10 @@ def _cxx_obj_type_from(self: Self, samples=(), types=()) -> Any: if not isinstance(lookup[py_types], dict): return lookup[py_types] lookup = lookup[py_types] - cpp_types = tuple([type(to_cxx(x)) for x in samples] + list(types)) - if cpp_types not in lookup: + cxx_types = tuple([type(to_cxx(x)) for x in samples] + list(types)) + if cxx_types not in lookup: raise ValueError( - f"unexpected keyword argument combination {cpp_types}, " + f"unexpected keyword argument combination {cxx_types}, " f"expected one of {lookup.keys()}" ) - return lookup[cpp_types] + return lookup[cxx_types] diff --git a/libsemigroups_pybind11/froidure_pin.py b/libsemigroups_pybind11/froidure_pin.py index c1727559..45dc955c 100644 --- a/libsemigroups_pybind11/froidure_pin.py +++ b/libsemigroups_pybind11/froidure_pin.py @@ -96,7 +96,7 @@ def wrapper(self, *args): class FroidurePin(CxxWrapper): # pylint: disable=missing-class-docstring - py_to_cxx_type_dict = { + _py_to_cxx_type_dict = { (_Transf1,): _FroidurePinTransf1, (_Transf2,): _FroidurePinTransf2, (_Transf4,): _FroidurePinTransf4, @@ -123,7 +123,10 @@ class FroidurePin(CxxWrapper): # pylint: disable=missing-class-docstring # C++ FroidurePin special methods ######################################################################## - def __init__( # pylint: disable=super-init-not-called + # TODO(1): This __init__ is identical to the SchreierSims __init__. It would + # probably be best to make an abstract base class from which all classes + # that construct using a list of generators inherit. + def __init__( # pylint: disable=super-init-not-called, duplicate-code self: Self, *args ) -> None: if len(args) == 0: @@ -132,12 +135,12 @@ def __init__( # pylint: disable=super-init-not-called gens = args[0] else: gens = args - cpp_obj_t = self._cxx_obj_type_from( + cxx_obj_t = self._cxx_obj_type_from( samples=(to_cxx(gens[0]),), ) self.Element = type(gens[0]) - self._cxx_obj = cpp_obj_t([to_cxx(x) for x in gens]) + self._cxx_obj = cxx_obj_t([to_cxx(x) for x in gens]) @_returns_element def __getitem__(self: Self, i: int) -> Element: @@ -154,10 +157,14 @@ def __iter__(self: Self) -> Iterator: ######################################################################## def current_elements(self: Self) -> Iterator: - return map(lambda x: to_py(self.Element, x), self._cxx_obj.current_elements()) + return map( + lambda x: to_py(self.Element, x), self._cxx_obj.current_elements() + ) def idempotents(self: Self) -> Iterator: - return map(lambda x: to_py(self.Element, x), self._cxx_obj.idempotents()) + return map( + lambda x: to_py(self.Element, x), self._cxx_obj.idempotents() + ) def sorted_elements(self: Self) -> Iterator: return map( diff --git a/libsemigroups_pybind11/schreier_sims.py b/libsemigroups_pybind11/schreier_sims.py new file mode 100644 index 00000000..4970d6fb --- /dev/null +++ b/libsemigroups_pybind11/schreier_sims.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Joseph Edwards +# +# Distributed under the terms of the GPL license version 3. +# +# The full license is in the file LICENSE, distributed with this software. + +# pylint: disable=no-name-in-module, invalid-name, unused-import, fixme +# pylint: disable=missing-function-docstring + +""" +This package provides the user-facing python part of libsemigroups_pybind11 for +the schreier_sims namespace from libsemigroups. +""" + +from functools import wraps +from typing import TypeVar as _TypeVar +from typing_extensions import Self + +from _libsemigroups_pybind11 import ( + intersection as _intersection, + SchreierSimsPerm1 as _SchreierSimsPerm1, + SchreierSimsPerm2 as _SchreierSimsPerm2, + Perm1 as _Perm1, + Perm2 as _Perm2, + # Perm4 as _Perm4, +) + +from .detail.cxx_wrapper import ( + to_cxx, + to_py, + CxxWrapper, +) + +Element = _TypeVar("Element") + +######################################################################## +# Decorators +######################################################################## + + +def _returns_element(method): + @wraps(method) + def wrapper(self, *args): + return to_py(self.Element, method(self, *args)) + + return wrapper + + +class SchreierSims(CxxWrapper): # pylint: disable=missing-class-docstring + _py_to_cxx_type_dict = { + (_Perm1,): _SchreierSimsPerm1, + (_Perm2,): _SchreierSimsPerm2, + # (_Perm4,): _SchreierSims, + } + + ######################################################################## + # C++ Constructors + ######################################################################## + + # TODO(1): This __init__ is identical to the FroidurePin __init__. It would + # probably be best to make an abstract base class from which all classes + # that construct using a list of generators inherit. + def __init__( # pylint: disable=super-init-not-called, duplicate-code + self: Self, *args + ) -> None: + if len(args) == 0: + raise ValueError("expected at least 1 argument, found 0") + if isinstance(args[0], list) and len(args) == 1: + gens = args[0] + else: + gens = args + cxx_obj_t = self._cxx_obj_type_from( + samples=(to_cxx(gens[0]),), + ) + self.Element = type(gens[0]) + + self._cxx_obj = cxx_obj_t() + for gen in gens: + self._cxx_obj.add_generator(to_cxx(gen)) + + ######################################################################## + # Methods returning an element + ######################################################################## + + @_returns_element + def generator(self: Self, index: int) -> Element: + return self._cxx_obj.generator(index) + + @_returns_element + def inverse_transversal_element(self: Self, depth: int, pt: int) -> Element: + return self._cxx_obj.inverse_transversal_element(depth, pt) + + @_returns_element + def one(self: Self) -> Element: + return self._cxx_obj.one() + + @_returns_element + def sift(self: Self, x: Element) -> Element: + return self._cxx_obj.sift(to_cxx(x)) + + @_returns_element + def strong_generator(self: Self, depth: int, index: int) -> Element: + return self._cxx_obj.strong_generator(depth, index) + + @_returns_element + def transversal_element(self: Self, depth: int, pt: int) -> Element: + return self._cxx_obj.transversal_element(depth, pt) + + +######################################################################## +# Helpers -- from schreier-sims.cpp +######################################################################## + + +def intersection(U: SchreierSims, S: SchreierSims, T: SchreierSims): + return _intersection(to_cxx(U), to_cxx(S), to_cxx(T)) diff --git a/src/main.cpp b/src/main.cpp index a110aeaa..b49da8d4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -151,7 +151,7 @@ The valid values are: m.attr("LIBSEMIGROUPS_EIGEN_ENABLED") = static_cast(LIBSEMIGROUPS_EIGEN_ENABLED); #else - m.attr("LIBSEMIGROUPS_EIGEN_ENABLED") = false; + m.attr("LIBSEMIGROUPS_EIGEN_ENABLED") = false; #endif #ifdef LIBSEMIGROUPS_HPCOMBI_ENABLED @@ -206,11 +206,12 @@ The valid values are: init_froidure_pin_base(m); init_ukkonen(m); init_froidure_pin(m); + init_schreier_sims(m); #ifdef VERSION_INFO m.attr("__version__") = VERSION_INFO; #else - m.attr("__version__") = "dev"; + m.attr("__version__") = "dev"; #endif //////////////////////////////////////////////////////////////////////// diff --git a/src/main.hpp b/src/main.hpp index 24b1d70b..e70feeb4 100644 --- a/src/main.hpp +++ b/src/main.hpp @@ -54,6 +54,7 @@ namespace libsemigroups { void init_froidure_pin_base(py::module&); void init_ukkonen(py::module&); void init_froidure_pin(py::module&); + void init_schreier_sims(py::module&); } // namespace libsemigroups diff --git a/src/schreier-sims.cpp b/src/schreier-sims.cpp new file mode 100644 index 00000000..9257b85e --- /dev/null +++ b/src/schreier-sims.cpp @@ -0,0 +1,461 @@ + +// +// libsemigroups_pybind11 +// Copyright (C) 2024 Joseph Edwards +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +// TODO(0) Check types + +// C std headers.... +// TODO complete or delete + +// C++ stl headers.... +#include // for allocator, make_unique, unique_ptr + +// libsemigroups headers +#include +#include + +// pybind11.... +#include +// #include +// #include +// #include +// TODO uncomment/delete + +// libsemigroups_pybind11.... +#include "main.hpp" // for init_schreier_sims + +namespace py = pybind11; + +namespace libsemigroups { + + namespace { + template + void bind_schreier_sims(py::module& m, std::string const& name) { + using SchreierSims_ = SchreierSims; + + std::string pyclass_name = std::string("SchreierSims") + name; + + py::class_ thing(m, + pyclass_name.c_str(), + R"pbdoc( +This class implements a deterministic version of the Schreier-Sims algorithm +acting on a relatively small number of points (< 1000). + +:example: + +.. doctest:: python + + >>> from libsemigroups_pybind11 import SchreierSims, Perm + >>> p1 = Perm([1, 0, 2, 3, 4] + list(range(5, 255))) + >>> p2 = Perm([1, 2, 3, 4, 0] + list(range(5, 255))) + >>> S = SchreierSims(p1, p2) + >>> S.size() + 120 +)pbdoc"); + thing.def("__repr__", [](SchreierSims_ const& S) { + return to_human_readable_repr(S); + }); + thing.def(py::init<>(), R"pbdoc( +:sig=(self: SchreierSimsPerm1, gens: List[Element]) -> None: +Construct from a list of generators. + +This function constructs a :any:`SchreierSimsPerm1` instance with generators in +the list *gens*. + +:param gens: the list of generators. +:type gens: List[Element] + +:raises LibsemigroupsError: if the generators do not have degree equal to + :math:`255` or :math:`511`, or the number of generators exceeds the + maximum capacity. +)pbdoc"); + + thing.def(py::init(), R"pbdoc( + Default copy constructor. +)pbdoc"); + thing.def("__copy__", + [](SchreierSims_ const& S) { return SchreierSims_(S); }); + thing.def("add_base_point", + &SchreierSims_::add_base_point, + py::arg("pt"), + R"pbdoc( +Add a base point to the stabiliser chain. + +:param pt: the base point to add. +:type pt: int + +:raises LibsemigroupsError: if *pt* is out of range. + +:raises LibsemigroupsError: if *pt* is already a base point. + +:raises LibsemigroupsError: if :any:`SchreierSimsPerm1.finished()` returns ``True``. + +:complexity: Linear in the current number of base points.)pbdoc"); + thing.def("add_generator", + &SchreierSims_::add_generator, + py::arg("x"), + R"pbdoc( +Add a generator. + +This functions adds the argument *x* as a new generator if and only if *x* is +not already an element of the group represented by the Schreier-Sims object. + +:param x: the generator to add. +:type x: Element + +:returns: ``True`` if *x* is added as a generator and ``False`` if it is not. +:rtype: bool + +:raises LibsemigroupsError: if the degree of *x* is not equal to :math:`255` + or :math:`511`, or if ``self`` already contains the maximum number of + elements. + +:complexity: Constant +)pbdoc"); + thing.def("base", + &SchreierSims_::base, + py::arg("index"), + R"pbdoc( +Get a base point. + +This function gets the base point with a given index. + +:param index: the index of the base point. +:type index: int + +:returns: The base point with index *index*. +:rtype: int + +:raises LibsemigroupsError: if *index* is out of range. + +:complexity: Constant. + +)pbdoc"); + thing.def("base_size", + &SchreierSims_::base_size, + R"pbdoc( +Get the size of the current base. + +:returns: The base size. +:rtype: int + +:complexity: Constant. +)pbdoc"); + thing.def("contains", + &SchreierSims_::contains, + py::arg("x"), + R"pbdoc( +Test membership of an element. + +:param x: the possible element. +:type x: Element + +:returns: ``True`` if *element* is a contained in the :any:`SchreierSimsPerm1` + instance, and ``False`` otherwise. +:rtype: bool +)pbdoc"); + thing.def("currently_contains", + &SchreierSims_::currently_contains, + py::arg("x"), + R"pbdoc( +Test membership of an element without running. + +This function tests the membership of an element without running the algorithm. + +:param x: the possible element. +:type x: Element + +:returns: ``True`` if *element* is a contained in the :any:`SchreierSimsPerm1` + instance, and ``False`` otherwise. +:rtype: bool +)pbdoc"); + thing.def("empty", + &SchreierSims_::empty, + R"pbdoc( +Check if any generators have been added so far. + +:returns: ``True`` if ``number_of_generators() == 0`` and ``False`` otherwise. +:rtype: bool + +:complexity: Constant. +)pbdoc"); + thing.def("finished", + &SchreierSims_::finished, + R"pbdoc( +Check if the stabiliser chain is fully enumerated. + +:returns: ``True`` if the stabiliser chain is fully enumerated and ``False`` otherwise. +:rtype: bool + +:complexity: Constant. +)pbdoc"); + thing.def("generator", + &SchreierSims_::generator, + py::arg("index"), + R"pbdoc( +Get a generator. + +This function returns the generator with a given index. + +:param index: the index of the generator to return. +:type index: int + +:returns: The generator with index *index*. +:rtype: Element + +:raises LibsemigroupsError: if the *index* is out of bounds. + +:complexity: Constant. +)pbdoc"); + thing.def("init", + &SchreierSims_::init, + R"pbdoc( +Reset to the trivial group. + +This function removes all generators, and orbits, and resets ``self`` so that it +represents the trivial group, as if ``self`` had been newly constructed. + +:complexity: Constant. +)pbdoc"); + thing.def("inverse_transversal_element", + &SchreierSims_::inverse_transversal_element, + py::arg("depth"), + py::arg("pt"), + R"pbdoc( +Get an inverse of a transversal element. + +This function returns the transversal element at depth *depth* which sends *pt* +to the basepoint. + +:param depth: the depth. +:type depth: int + +:param pt: the point to map to the base point under the inverse transversal + element. +:type pt: int + +:returns: the inverse transversal element. +:rtype: Element + +:raises LibsemigroupsError: if the *depth* is out of bounds. + +:raises LibsemigroupsError: if *pt* is not in the orbit of the basepoint. + +:complexity: Constant. +)pbdoc"); + thing.def("number_of_generators", + &SchreierSims_::number_of_generators, + R"pbdoc( +The number of generators. + +This function returns the number of generators. + +:returns: The number of generators. +:rtype: int + +:complexity: Constant. +)pbdoc"); + thing.def("number_of_strong_generators", + &SchreierSims_::number_of_strong_generators, + py::arg("depth"), + R"pbdoc( +The number of strong generators at a given depth. + +This function returns the number of strong generators of the stabiliser chain at +a given depth. + +:param depth: the depth. +:type depth: int + +:returns: The number of strong generators. +:rtype: int + +:raises LibsemigroupsError: if the *depth* is out of bounds. + +:complexity: Constant. +)pbdoc"); + thing.def("one", + &SchreierSims_::one, + R"pbdoc( +Returns a const reference to the identity. + +:returns: The identity element. +:rtype: Element +)pbdoc"); + thing.def("orbit_lookup", + &SchreierSims_::orbit_lookup, + py::arg("depth"), + py::arg("pt"), + R"pbdoc( +Check if a point is in the orbit of a basepoint. + +:param depth: the depth. +:type depth: int + +:param pt: the point. +:type pt: int + +:returns: ``True`` if the point *pt* is in the orbit of the basepoint of + ``self`` at depth *depth*, and ``False`` otherwise. +:rtype: bool + +:raises LibsemigroupsError: if the *depth*` is out of bounds or if *pt* is out + of bounds. + +:complexity: Constant. +)pbdoc"); + thing.def("run", + &SchreierSims_::run, + R"pbdoc( +Run the Schreier-Sims algorithm. + + +:complexity: :math:`O(N^2\log^3|G|+|T|N^2\log|G|)` time and + :math:`O(N^2\log|G|+|T|N)` space, where ``N`` is the degree of the + generators, :math:`|G|` is the size of the group and :math:`|T|` is the + number of generators of the group. +)pbdoc"); + thing.def("sift", + &SchreierSims_::sift, + py::arg("x"), + R"pbdoc( +Sift an element through the stabiliser chain. + +:param x: A group element. +:type x: Element + +:returns: A sifted element. +:rtype: Element + +:raises LibsemigroupsError: if the degree of *x* is not equal to the degree of + the generators. +)pbdoc"); + thing.def("sift_inplace", + &SchreierSims_::sift_inplace, + py::arg("x"), + R"pbdoc( +Sift an element through the stabiliser chain in-place. + +:param x: a group element. +:type x: Element + +:raises LibsemigroupsError: if the degree of *x* is not equal to the degree of + the generators. +)pbdoc"); + thing.def("size", + &SchreierSims_::size, + R"pbdoc( +Returns the size of the group represented by ``self``. + +:returns: the size of the group. +:rtype: int +)pbdoc"); + thing.def("current_size", + &SchreierSims_::current_size, + R"pbdoc( +Returns the size of the group represented by this, without running the algorithm. + +:returns: the size of the group. +:rtype: int +)pbdoc"); + thing.def("strong_generator", + &SchreierSims_::strong_generator, + py::arg("depth"), + py::arg("index"), + R"pbdoc( +Get a strong generator. + +This function returns the generator with a given depth and index. + +:param depth: the depth. +:type depth: int + +:param index: the index of the generator to return. +:type index: int + +:returns: The strong generator of at depth *depth* and with index *index*. +:rtype: Element + +:raises LibsemigroupsError: if the *depth* is out of bounds. + +:raises LibsemigroupsError: if the *index* is out of bounds. + +:complexity: Constant. +)pbdoc"); + thing.def("transversal_element", + &SchreierSims_::transversal_element, + py::arg("depth"), + py::arg("pt"), + R"pbdoc( +Get an transversal element. + +This function returns the transversal element at depth *depth* which sends the +corresponding basepoint to the point *pt*. + +:param depth: the depth. +:type depth: int + +:param pt: the image of the base point under the traversal. +:type pt: int + +:returns: The transversal element. +:rtype: Element + +:raises LibsemigroupsError: if *depth* is out of bounds. + +:raises LibsemigroupsError: if *pt* is not in the orbit of the basepoint. + +:complexity: Constant. +)pbdoc"); + + m.def( + "intersection", + [](SchreierSims_& T, SchreierSims_& S1, SchreierSims_& S2) { + return schreier_sims::intersection(T, S1, S2); + }, + py::arg("T"), + py::arg("S1"), + py::arg("S2"), + R"pbdoc( +Find the intersection of two permutation groups. + +This function finds the intersection of two permutation groups. +It modifies the first parameter *T* to be the :any:`SchreierSimsPerm1` object +corresponding to the intersection of *S1* and *S2*. + +:param T: an empty SchreierSims object that will hold the result. +:type T: SchreierSimsPerm1 + +:param S1: the first group of the intersection. +:type S1: SchreierSimsPerm1 + +:param S2: the second group of the intersection. +:type S2: SchreierSimsPerm1 + +:raises LibsemigroupsError: if *T* is not empty. +)pbdoc"); + } // bind_schreier_sims + } // namespace + + void init_schreier_sims(py::module& m) { + // One call to bind is required per list of types + bind_schreier_sims<255, uint8_t, Perm<0, uint8_t>>(m, "Perm1"); + bind_schreier_sims<511, uint16_t, Perm<0, uint16_t>>(m, "Perm2"); + } + +} // namespace libsemigroups diff --git a/tests/test_schreier_sims.py b/tests/test_schreier_sims.py new file mode 100644 index 00000000..a92cccd4 --- /dev/null +++ b/tests/test_schreier_sims.py @@ -0,0 +1,562 @@ +# -*- coding: utf-8 -*- +# pylint: disable=no-name-in-module, missing-function-docstring +# pylint: disable=missing-class-docstring, missing-module-docstring +# pylint: disable=invalid-name, redefined-outer-name + +# Copyright (c) 2024 Joseph Edwards +# +# Distributed under the terms of the GPL license version 3. +# +# The full license is in the file LICENSE, distributed with this software. + +# TODO(0): +# * test number_of_strong_generators +# * test strong_generator + +from copy import copy +import pytest + +from libsemigroups_pybind11 import ( + Perm, + LibsemigroupsError, + SchreierSims, + ReportGuard, +) + +from libsemigroups_pybind11.schreier_sims import intersection + + +def check_constructors(gens): + ReportGuard(False) + # default constructor + with pytest.raises(ValueError): + SchreierSims() + + S1 = SchreierSims(gens) + + # copy constructor + S2 = copy(S1) + + assert S1 is not S2 + assert S1.number_of_generators() == S2.number_of_generators() + assert S1.current_size() == S2.current_size() + assert S1.finished() == S2.finished() + + +def check_generators(gens): + ReportGuard(False) + S = SchreierSims(gens) + for i, gen in enumerate(gens): + assert S.generator(i) == gen + + with pytest.raises(LibsemigroupsError): + S.generator(len(gens)) + + assert S.number_of_generators() == len(gens) + + U = SchreierSims(gens[0]) + for x in gens[1:]: + U.add_generator(x) + assert S.number_of_generators() == U.number_of_generators() + assert S.size() == U.size() + + +def check_empty(gens): + ReportGuard(False) + S = SchreierSims(gens) + assert not S.empty() + S.init() + assert S.empty() + + +def check_finished(gens): + ReportGuard(False) + S = SchreierSims(gens) + assert not S.finished() + S.run() + assert S.finished() + + +def check_one(n): + S = SchreierSims([Perm(range(n))]) + assert S.contains(S.one()) + assert S.one() == Perm(range(n)) + + +def check_elements(n): + ReportGuard(False) + S = SchreierSims([Perm(range(n))]) + + S.add_base_point(0) + S.add_base_point(1) + S.add_base_point(2) + S.add_generator( + Perm( + [ + 0, + 2, + 59, + 57, + 16, + 18, + 43, + 41, + 36, + 38, + 31, + 29, + 52, + 54, + 15, + 13, + 8, + 10, + 51, + 49, + 24, + 26, + 35, + 33, + 44, + 46, + 23, + 21, + 60, + 62, + 7, + 5, + 32, + 34, + 27, + 25, + 48, + 50, + 11, + 9, + 4, + 6, + 63, + 61, + 20, + 22, + 47, + 45, + 40, + 42, + 19, + 17, + 56, + 58, + 3, + 1, + 12, + 14, + 55, + 53, + 28, + 30, + 39, + 37, + ] + + list((range(64, n))) + ) + ) + S.add_generator( + Perm( + [ + 0, + 40, + 51, + 27, + 1, + 41, + 50, + 26, + 2, + 42, + 49, + 25, + 3, + 43, + 48, + 24, + 4, + 44, + 55, + 31, + 5, + 45, + 54, + 30, + 6, + 46, + 53, + 29, + 7, + 47, + 52, + 28, + 16, + 56, + 35, + 11, + 17, + 57, + 34, + 10, + 18, + 58, + 33, + 9, + 19, + 59, + 32, + 8, + 20, + 60, + 39, + 15, + 21, + 61, + 38, + 14, + 22, + 62, + 37, + 13, + 23, + 63, + 36, + 12, + ] + + list(range(64, n)) + ) + ) + S.add_generator( + Perm( + [ + 1, + 0, + 3, + 2, + 5, + 4, + 7, + 6, + 9, + 8, + 11, + 10, + 13, + 12, + 15, + 14, + 17, + 16, + 19, + 18, + 21, + 20, + 23, + 22, + 25, + 24, + 27, + 26, + 29, + 28, + 31, + 30, + 33, + 32, + 35, + 34, + 37, + 36, + 39, + 38, + 41, + 40, + 43, + 42, + 45, + 44, + 47, + 46, + 49, + 48, + 51, + 50, + 53, + 52, + 55, + 54, + 57, + 56, + 59, + 58, + 61, + 60, + 63, + 62, + ] + + list(range(64, n)) + ) + ) + S.run() + + with pytest.raises(LibsemigroupsError): + S.transversal_element(3, 0) + with pytest.raises(LibsemigroupsError): + S.transversal_element(4, 0) + with pytest.raises(LibsemigroupsError): + S.transversal_element(0, 64) + with pytest.raises(LibsemigroupsError): + S.transversal_element(0, 65) + with pytest.raises(LibsemigroupsError): + S.inverse_transversal_element(3, 0) + with pytest.raises(LibsemigroupsError): + S.inverse_transversal_element(4, 0) + with pytest.raises(LibsemigroupsError): + S.inverse_transversal_element(0, 64) + with pytest.raises(LibsemigroupsError): + S.inverse_transversal_element(0, 65) + + for i in range(3): + for j in range(64): + if S.orbit_lookup(i, j): + assert S.transversal_element(i, j)[S.base(i)] == j + assert S.inverse_transversal_element(i, j)[j] == S.base(i) + else: + with pytest.raises(LibsemigroupsError): + S.transversal_element(i, j) + with pytest.raises(LibsemigroupsError): + S.inverse_transversal_element(i, j) + + +def check_sift(gens): + ReportGuard(False) + S = SchreierSims(gens) + S.run() + for i, gen in enumerate(gens): + assert S.generator(i) == gen + assert list(S.sift(gen)) == list(S.one()) + + +def check_sift_inplace(gens): + ReportGuard(False) + S = SchreierSims(gens) + one = S.one() + S.run() + for gen in gens: + S.sift_inplace(gen) + assert all(gen == one for gen in gens) + + +def check_intersection(n): + ReportGuard(False) + gens_S = [ + Perm([1, 3, 7, 5, 2, 0, 4, 6] + list(range(8, n))), + Perm([2, 4, 3, 6, 5, 7, 0, 1] + list(range(8, n))), + Perm([3, 5, 6, 0, 7, 1, 2, 4] + list(range(8, n))), + ] + gens_T = [ + Perm([1, 0, 7, 5, 6, 3, 4, 2] + list(range(8, n))), + Perm([2, 4, 3, 6, 5, 7, 0, 1] + list(range(8, n))), + Perm([3, 5, 6, 0, 7, 1, 2, 4] + list(range(8, n))), + ] + gens_U = [Perm(list(range(n)))] + + S = SchreierSims(gens_S) + T = SchreierSims(gens_T) + U = SchreierSims(gens_U) + intersection(U, S, T) + assert U.size() == 4 + assert U.contains(Perm([2, 4, 3, 6, 5, 7, 0, 1] + list(range(8, n)))) + + +def check_SchreierSims_001(n): + ReportGuard(False) + S = SchreierSims([Perm(range(n))]) + S.init() + assert S.size() == 1 + S.add_generator( + Perm( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 0] + + list(range(17, n)) + ) + ) + S.add_generator( + Perm( + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 14] + + list(range(17, n)) + ) + ) + + assert not S.currently_contains( + Perm( + [1, 0, 3, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + + list(range(17, n)) + ) + ) + assert S.current_size() == 17 + assert S.size() == 177843714048000 + assert S.base(0) == 0 + assert S.contains( + Perm( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 0] + + list(range(17, n)) + ) + ) + + assert not S.contains( + Perm( + [1, 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + + list(range(17, n)) + ) + ) + assert S.contains( + Perm( + [1, 0, 3, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + + list(range(17, n)) + ) + ) + + S.init() + with pytest.raises(LibsemigroupsError): + S.base(0) + with pytest.raises(LibsemigroupsError if n != 255 else TypeError): + S.add_base_point(n + 1) + S.add_base_point(14) + S.add_base_point(15) + S.add_generator( + Perm( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 0] + + list(range(17, n)) + ) + ) + S.add_base_point(1) + S.add_base_point(3) + S.add_generator( + Perm( + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 14] + + list(range(17, n)) + ) + ) + assert S.base_size() == 4 + assert S.size() == 177843714048000 + assert S.base(0) == 14 + assert S.base(1) == 15 + assert S.base(2) == 1 + assert S.base(3) == 3 + assert S.base_size() == 15 + + with pytest.raises(LibsemigroupsError): + S.add_base_point(1) + with pytest.raises(LibsemigroupsError): + S.base(15) + + assert S.contains( + Perm( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 0] + + list(range(17, n)) + ) + ) + assert not S.contains( + Perm( + [1, 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + + list(range(17, n)) + ) + ) + assert S.contains( + Perm( + [1, 0, 3, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + + list(range(17, n)) + ) + ) + with pytest.raises(LibsemigroupsError): + S.add_base_point(1) + + S.init() + S.add_generator( + Perm( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 0] + + list(range(17, n)) + ) + ) + assert S.size() == 17 + S.add_generator( + Perm( + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 14] + + list(range(17, n)) + ) + ) + assert S.size() == 177843714048000 + + +@pytest.fixture +def checks_with_generators(): + return ( + check_constructors, + check_generators, + check_empty, + check_finished, + check_sift, + check_sift_inplace, + ) + + +@pytest.fixture +def checks_with_int(): + return ( + check_SchreierSims_001, + check_one, + check_elements, + check_intersection, + ) + + +def test_SchreierSims_001(checks_with_generators): + gens = [ + Perm([1, 0, 2, 3, 4] + list(range(5, 255))), + Perm([1, 2, 3, 4, 0] + list(range(5, 255))), + ] + for check in checks_with_generators: + check(gens) + + +def test_SchreierSims_002(checks_with_generators): + gens = [ + Perm([0, 2, 4, 6, 7, 3, 8, 1, 5] + list(range(9, 255))), + Perm([0, 3, 5, 4, 8, 7, 2, 6, 1] + list(range(9, 255))), + ] + for check in checks_with_generators: + check(gens) + + +def test_SchreierSims_003(checks_with_generators): + gens = [ + Perm([1, 0, 2, 3, 4] + list(range(5, 511))), + Perm([1, 2, 3, 4, 0] + list(range(5, 511))), + ] + for check in checks_with_generators: + check(gens) + + +def test_SchreierSims_004(checks_with_generators): + gens = [ + Perm([0, 2, 4, 6, 7, 3, 8, 1, 5] + list(range(9, 511))), + Perm([0, 3, 5, 4, 8, 7, 2, 6, 1] + list(range(9, 511))), + ] + for check in checks_with_generators: + check(gens) + + +def test_SchreierSims_Perm1(checks_with_int): + for check in checks_with_int: + check(255) + + +def test_SchreierSims_Perm2(checks_with_int): + for check in checks_with_int: + check(511)