From 74672c63f7e5a214276c1f50631a345132551952 Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Mon, 17 Aug 2026 11:28:37 +0100 Subject: [PATCH 01/27] order: add support for LenLexCmp --- .../data-structures/order/functions.rst | 74 +++ docs/source/data-structures/order/index.rst | 68 +-- .../data-structures/order/lenlex-cmp.rst | 34 ++ docs/source/data-structures/order/lex-cmp.rst | 34 ++ src/libsemigroups_pybind11/__init__.py | 13 +- src/libsemigroups_pybind11/order.py | 152 +++++- src/order.cpp | 467 +++++++++++++++++- tests/test_order.py | 108 ++++ 8 files changed, 883 insertions(+), 67 deletions(-) create mode 100644 docs/source/data-structures/order/functions.rst create mode 100644 docs/source/data-structures/order/lenlex-cmp.rst create mode 100644 docs/source/data-structures/order/lex-cmp.rst diff --git a/docs/source/data-structures/order/functions.rst b/docs/source/data-structures/order/functions.rst new file mode 100644 index 00000000..db23523c --- /dev/null +++ b/docs/source/data-structures/order/functions.rst @@ -0,0 +1,74 @@ +.. + Copyright (c) 2024-2026 Joseph Edwards, 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 + +Functions for orders +==================== + +This page contains the documentation for functions that compare words or +strings with respect to certain reduction orderings. + +.. important:: + + The following functions are deprecated in v1.5.0 and will be removed v2.0.0: + + * ``lexicographical_compare``, please use :any:`lex_cmp` instead. + * ``recursive_path_compare`` please use :any:`rev_rpo_cmp` instead. + * ``shortlex_compare`` please use :any:`lenlex_cmp` instead. + +Contents +-------- + +.. autosummary:: + :signatures: short + + len_wt_lex_cmp + lenlex_cmp + lex_cmp + rev_len_wt_lex_cmp + rev_lenlex_cmp + rev_lex_cmp + rev_rpo_cmp + rev_wr_cmp + rev_wt_lenlex_cmp + rev_wt_lex_cmp + rpo_cmp + wr_cmp + wt_lenlex_cmp + wt_lex_cmp + +Full API +-------- + +.. autofunction:: len_wt_lex_cmp + +.. autofunction:: lenlex_cmp + +.. autofunction:: lex_cmp + +.. autofunction:: rev_len_wt_lex_cmp + +.. autofunction:: rev_lenlex_cmp + +.. autofunction:: rev_lex_cmp + +.. autofunction:: rev_rpo_cmp + +.. autofunction:: rev_wr_cmp + +.. autofunction:: rev_wt_lenlex_cmp + +.. autofunction:: rev_wt_lex_cmp + +.. autofunction:: rpo_cmp + +.. autofunction:: wr_cmp + +.. autofunction:: wt_lenlex_cmp + +.. autofunction:: wt_lex_cmp diff --git a/docs/source/data-structures/order/index.rst b/docs/source/data-structures/order/index.rst index 30a0a68e..ecacac07 100644 --- a/docs/source/data-structures/order/index.rst +++ b/docs/source/data-structures/order/index.rst @@ -10,70 +10,20 @@ Orders ====== -This page contains the documentation for several functions for comparing words -or strings with respect to certain reduction orderings. +This page contains the documentation for classes and several functions for +comparing words or strings with respect to certain reduction orderings. .. seealso:: - :any:`Order` - :any:`Alphabet` -.. important:: +The class and functions in ``libsemigroups_pybind11`` for ordering words and +strings are described on the following pages: - The following functions are deprecated in v1.5.0 and will be removed v2.0.0: +.. toctree:: + :maxdepth: 1 - * ``lexicographical_compare``, please use :any:`lex_cmp` instead. - * ``recursive_path_compare`` please use :any:`rev_rpo_cmp` instead. - * ``shortlex_compare`` please use :any:`lenlex_cmp` instead. - -Contents --------- - -.. autosummary:: - :signatures: short - - len_wt_lex_cmp - lenlex_cmp - lex_cmp - rev_len_wt_lex_cmp - rev_lenlex_cmp - rev_lex_cmp - rev_rpo_cmp - rev_wr_cmp - rev_wt_lenlex_cmp - rev_wt_lex_cmp - rpo_cmp - wr_cmp - wt_lenlex_cmp - wt_lex_cmp - -Full API --------- - -.. autofunction:: len_wt_lex_cmp - -.. autofunction:: lenlex_cmp - -.. autofunction:: lex_cmp - -.. autofunction:: rev_len_wt_lex_cmp - -.. autofunction:: rev_lenlex_cmp - -.. autofunction:: rev_lex_cmp - -.. autofunction:: rev_rpo_cmp - -.. autofunction:: rev_wr_cmp - -.. autofunction:: rev_wt_lenlex_cmp - -.. autofunction:: rev_wt_lex_cmp - -.. autofunction:: rpo_cmp - -.. autofunction:: wr_cmp - -.. autofunction:: wt_lenlex_cmp - -.. autofunction:: wt_lex_cmp + lex-cmp + lenlex-cmp + functions diff --git a/docs/source/data-structures/order/lenlex-cmp.rst b/docs/source/data-structures/order/lenlex-cmp.rst new file mode 100644 index 00000000..f2b9c172 --- /dev/null +++ b/docs/source/data-structures/order/lenlex-cmp.rst @@ -0,0 +1,34 @@ +.. + Copyright (c) 2026 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 LenLexCmp class +=================== + +.. autoclass:: LenLexCmp + :doc-only: + +Contents +-------- + +.. autosummary:: + :signatures: short + + ~LenLexCmp + LenLexCmp.__call__ + LenLexCmp.alphabet + LenLexCmp.copy + LenLexCmp.init + +Full API +-------- + +.. autoclass:: LenLexCmp + :class-doc-from: init + :special-members: __call__ + :members: diff --git a/docs/source/data-structures/order/lex-cmp.rst b/docs/source/data-structures/order/lex-cmp.rst new file mode 100644 index 00000000..2a07dbaa --- /dev/null +++ b/docs/source/data-structures/order/lex-cmp.rst @@ -0,0 +1,34 @@ +.. + Copyright (c) 2026 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 LexCmp class +================ + +.. autoclass:: LexCmp + :doc-only: + +Contents +-------- + +.. autosummary:: + :signatures: short + + ~LexCmp + LexCmp.__call__ + LexCmp.alphabet + LexCmp.copy + LexCmp.init + +Full API +-------- + +.. autoclass:: LexCmp + :class-doc-from: init + :special-members: __call__ + :members: diff --git a/src/libsemigroups_pybind11/__init__.py b/src/libsemigroups_pybind11/__init__.py index 212f2498..55198ba2 100644 --- a/src/libsemigroups_pybind11/__init__.py +++ b/src/libsemigroups_pybind11/__init__.py @@ -53,10 +53,17 @@ from .knuth_bendix import KnuthBendix from .konieczny import Konieczny from .matrix import Matrix, MatrixKind -from .order import wr_cmp +from .order import LenLexCmp, LexCmp, wr_cmp from .presentation import InversePresentation, Presentation from .schreier_sims import SchreierSims -from .sims import MinimalRepOrc, RepOrc, Sims1, Sims2, SimsRefinerFaithful, SimsRefinerIdeals +from .sims import ( + MinimalRepOrc, + RepOrc, + Sims1, + Sims2, + SimsRefinerFaithful, + SimsRefinerIdeals, +) from .stephen import Stephen from .to import to from .todd_coxeter import ToddCoxeter @@ -168,6 +175,8 @@ "Joiner", "LibsemigroupsError", "LimitMax", + "LenLexCmp", + "LexCmp", "Meeter", "NegativeInfinity", "Order", diff --git a/src/libsemigroups_pybind11/order.py b/src/libsemigroups_pybind11/order.py index 3fc4074f..449379ee 100644 --- a/src/libsemigroups_pybind11/order.py +++ b/src/libsemigroups_pybind11/order.py @@ -4,12 +4,156 @@ # # The full license is in the file LICENSE, distributed with this software. -"""Word-ordering comparison functions.""" +"""Python wrappers for word-ordering objects and comparison functions.""" -from _libsemigroups_pybind11 import wr_cmp as _wr_cmp +from typing_extensions import Self as _Self -from .detail.cxx_wrapper import wrap_cxx_free_fn as _wrap_cxx_free_fn +from _libsemigroups_pybind11 import ( + LenLexCmpDefault as _LenLexCmpDefault, + LenLexCmpString as _LenLexCmpString, + LenLexCmpWord as _LenLexCmpWord, + LexCmpDefault as _LexCmpDefault, + LexCmpString as _LexCmpString, + LexCmpWord as _LexCmpWord, + wr_cmp as _wr_cmp, +) + +from .alphabet import Alphabet as _Alphabet +from .detail.cxx_wrapper import ( + CxxWrapper as _CxxWrapper, + copy_cxx_mem_fns as _copy_cxx_mem_fns, + register_cxx_wrapped_type as _register_cxx_wrapped_type, + to_cxx as _to_cxx, + wrap_cxx_free_fn as _wrap_cxx_free_fn, +) +from .detail.decorators import copydoc as _copydoc + + +class LexCmp(_CxxWrapper): + __doc__ = _LexCmpString.__doc__ + + _py_template_params_to_cxx_type = { + (): _LexCmpDefault, + (str,): _LexCmpString, + (list[int],): _LexCmpWord, + } + + _cxx_type_to_py_template_params = dict( + zip( + _py_template_params_to_cxx_type.values(), + _py_template_params_to_cxx_type.keys(), + strict=True, + ) + ) + + _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} + + @_copydoc(_LexCmpString.__init__, _LexCmpDefault.__init__) + def __init__(self: _Self, *args, **kwargs) -> None: + super().__init__(*args, optional_kwargs=("alphabet",), **kwargs) + if _to_cxx(self) is not None: + return + + if len(args) > 1: + raise TypeError( + f"expected at most 1 positional argument, found {len(args)}" + ) + + if len(args) == 1 and kwargs: + raise TypeError( + 'expected either 1 positional argument or the keyword argument "alphabet", ' + "but found both" + ) + + if len(args) == 1: + alphabet = args[0] + else: + alphabet = kwargs.get("alphabet") + + if len(args) == 0 and "alphabet" not in kwargs: + self.py_template_params = () + self.init_cxx_obj() + elif isinstance(alphabet, _Alphabet): + self.py_template_params = alphabet.py_template_params + self.init_cxx_obj(alphabet) + else: + raise TypeError( + f"expected the argument to be an Alphabet, but found {type(alphabet)}" + ) + + @_copydoc(_LexCmpString.__call__) + def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: + return super().__call__(x, y) + + +_copy_cxx_mem_fns(_LexCmpString, LexCmp) +_register_cxx_wrapped_type(_LexCmpDefault, LexCmp) +_register_cxx_wrapped_type(_LexCmpString, LexCmp) +_register_cxx_wrapped_type(_LexCmpWord, LexCmp) + + +class LenLexCmp(_CxxWrapper): + __doc__ = _LenLexCmpString.__doc__ + + _py_template_params_to_cxx_type = { + (): _LenLexCmpDefault, + (str,): _LenLexCmpString, + (list[int],): _LenLexCmpWord, + } + + _cxx_type_to_py_template_params = dict( + zip( + _py_template_params_to_cxx_type.values(), + _py_template_params_to_cxx_type.keys(), + strict=True, + ) + ) + + _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} + + @_copydoc(_LenLexCmpString.__init__, _LenLexCmpDefault.__init__) + def __init__(self: _Self, *args, **kwargs) -> None: + super().__init__(*args, optional_kwargs=("alphabet",), **kwargs) + if _to_cxx(self) is not None: + return + + if len(args) > 1: + raise TypeError( + f"expected at most 1 positional argument, found {len(args)}" + ) + + if len(args) == 1 and kwargs: + raise TypeError( + 'expected either 1 positional argument or the keyword argument "alphabet", ' + "but found both" + ) + + if len(args) == 1: + alphabet = args[0] + else: + alphabet = kwargs.get("alphabet") + + if len(args) == 0 and "alphabet" not in kwargs: + self.py_template_params = () + self.init_cxx_obj() + elif isinstance(alphabet, _Alphabet): + self.py_template_params = alphabet.py_template_params + self.init_cxx_obj(alphabet) + else: + raise TypeError( + f"expected the argument to be an Alphabet, but found {type(alphabet)}" + ) + + @_copydoc(_LenLexCmpString.__call__) + def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: + return super().__call__(x, y) + + +_copy_cxx_mem_fns(_LenLexCmpString, LenLexCmp) +_register_cxx_wrapped_type(_LenLexCmpDefault, LenLexCmp) +_register_cxx_wrapped_type(_LenLexCmpString, LenLexCmp) +_register_cxx_wrapped_type(_LenLexCmpWord, LenLexCmp) wr_cmp = _wrap_cxx_free_fn(_wr_cmp) -__all__ = ["wr_cmp"] +__all__ = ["LenLexCmp", "LexCmp", "wr_cmp"] diff --git a/src/order.cpp b/src/order.cpp index 54c6b99c..16339a44 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -17,8 +17,9 @@ // // C++ stl headers.... -#include // for string -#include // for vector +#include // for string +#include // for is_same_v +#include // for vector // libsemigroups.... #include // for *_cmp, Order @@ -142,6 +143,8 @@ Words are first ordered by length and then lexicographically. :returns: Whether *x* is less than *y*. :rtype: bool +.. seealso:: :any:`LenLexCmp` for a reusable len-lex comparison object. + .. doctest:: python >>> from libsemigroups_pybind11 import lenlex_cmp @@ -1230,6 +1233,457 @@ differences within one level are compared using lenlex ordering. True )pbdoc"); } + + //////////////////////////////////////////////////////////////////////// + // Functions for binding the various Cmp structs such as WtLenLexCmp, + // RPOCmp, and so on + //////////////////////////////////////////////////////////////////////// + + // The remaining documentation is included in the corresponding + // alphabet-aware binding function below. + template + void bind_cmp_default(py::module& m, std::string name) { + std::string arg_type(name); + name += "CmpDefault"; + arg_type += "Cmp"; + py::class_ thing(m, name.c_str()); + + thing.def(py::init<>(), + fmt::format(R"pbdoc( +:sig=(self: {0}) -> None: +Construct a comparison object. + +Constructs an object whose call operator compares either ``str`` or +``list[int]`` words using the natural order of their letters. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import {0} + >>> {0}()("a", "b") + True +)pbdoc", + arg_type) + .c_str()); + + thing.def("__repr__", + [](Cmp const& self) { return to_human_readable_repr(self); }); + + thing.def("__copy__", [](Cmp const& self) { return Cmp(self); }); + + thing.def("copy", [](Cmp const& self) { return Cmp(self); }); + + thing.def( + "__call__", + [](Cmp const& self, std::string const& x, std::string const& y) { + return self(x, y); + }, + py::arg("x"), + py::arg("y")); + + thing.def( + "__call__", + [](Cmp const& self, word_type const& x, word_type const& y) { + return self(x, y); + }, + py::arg("x"), + py::arg("y")); + } + + template + void bind_lex_cmp_with_alphabet(py::module& m, char const* name) { + using LexCmp_ = LexCmp; + + // The Python wrapper copies all documentation from this + // specialization, except those given above. + py::class_ thing(m, name, R"pbdoc( +Compare words lexicographically. + +Use ``LexCmp()`` to compare either ``str`` or ``list[int]`` words using the +natural order of their letters. Use ``LexCmp(alphabet)`` to compare words by +the positions of their letters in *alphabet*. The latter form copies +*alphabet* and only accepts words with the same type as *alphabet*. + +.. note:: + The constructor fixes whether this object is alphabet-aware (i.e. constructed + from an :any:`Alphabet` object). It also fixes the word type of an + alphabet-aware object. In particular, ``LexCmp()`` only accepts ``init()`` + (which does nothing); it cannot be changed into an alphabet-aware object by + calling ``init(alphabet)``. Similarly, + ``LexCmp(alphabet).init(new_alphabet)`` requires *new_alphabet* to have the + same word type as *alphabet*. + +.. seealso:: + + :any:`Alphabet` + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, LexCmp + >>> LexCmp()("a", "b") + True + >>> LexCmp()([0], [1]) + True + >>> LexCmp(Alphabet("ba"))("b", "a") + True +)pbdoc"); + + thing.def(py::init const&>(), + py::arg("alphabet"), + R"pbdoc( +:sig=(self: LexCmp, alphabet: Alphabet) -> None: +Construct a lexicographic comparison object from an alphabet. + +Constructs an object whose call operator compares words by their positions in +*alphabet*. The type of letters in *alphabet* also fixes the accepted word type +for the call operator. + +:param alphabet: the optional alphabet defining the order of letters. +:type alphabet: Alphabet + +:raises TypeError: + if more than one argument is given or the argument is not an :any:`Alphabet`. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, LexCmp + >>> LexCmp(Alphabet("ba"))("b", "a") + True +)pbdoc"); + + thing.def("__repr__", [](LexCmp_ const& self) { + return to_human_readable_repr(self); + }); + + thing.def("__copy__", [](LexCmp_ const& self) { return LexCmp_(self); }); + + thing.def( + "copy", + [](LexCmp_ const& self) { return LexCmp_(self); }, + R"pbdoc( +:sig=(self: LexCmp) -> LexCmp: +Copy a comparison object. + +The copy has the same mode and word type as *self*. For an alphabet-aware +object, the stored alphabet is also copied, so subsequently reinitializing one +comparison object does not affect the other. + +:returns: A copy of *self*. +:rtype: LexCmp + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, LexCmp + >>> LexCmp(Alphabet("ba")).copy()("b", "a") + True +)pbdoc"); + + thing.def( + "init", + [](LexCmp_& self, Alphabet const& alphabet) -> LexCmp_& { + return self.init(alphabet); + }, + py::arg("alphabet"), + R"pbdoc( +:sig=(self: LexCmp, alphabet: Alphabet) -> LexCmp: +Reinitialize the comparison object. + +If *self* was constructed using an :any:`Alphabet`, then ``init(new_alphabet)`` +puts *self* back into the same state it would have been had it been newly +constructed from *alphabet*. + +:param alphabet: the replacement alphabet. +:type alphabet: Alphabet + +:returns: The first argument *self*. +:rtype: LexCmp + +:raises TypeError: + if the type of the words in the new alphabet is not the same as the existing alphabet. +:raises AttributeError: + if *self* was constructed without an alphabet. + +.. warning:: + This method only works if *self* was constructed as + ``LexCmp(alphabet)``. An object constructed as ``LexCmp()`` does not have a + stored alphabet. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, LexCmp + >>> compare = LexCmp(Alphabet("ab")) + >>> compare.init(Alphabet("ba")) is compare + True + >>> compare("b", "a") + True +)pbdoc"); + + thing.def( + "__call__", + [](LexCmp_ const& self, Word const& x, Word const& y) { + return self(x, y); + }, + py::arg("x"), + py::arg("y"), + R"pbdoc( +:sig=(self: LexCmp, x: str | list[int], y: str | list[int]) -> bool: +Compare two words lexicographically. + +If *self* was constructed as ``LexCmp()``, then *x* and *y* must either both +be strings or both be lists of integers, and letters are compared using their +natural order. If *self* was constructed using an :any:`Alphabet`, then *x* and +*y* must have the same type of words as *alphabet*, and letters are compared by +their positions in the alphabet. + +:param x: the first word. +:type x: str | list[int] +:param y: the second word. +:type y: str | list[int] +:returns: Whether *x* is less than *y*. +:rtype: bool + +:raises TypeError: + if *x* and *y* are not both strings or both lists of integers, or if their + type does not match the alphabet used to construct *self*. +:raises LibsemigroupsError: + if *self* is alphabet-aware and either word contains a letter that does not + belong to its alphabet. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import LexCmp + >>> LexCmp()("ab", "ba") + True + >>> LexCmp()([0, 1], [1, 0]) + True +)pbdoc"); + + thing.def("alphabet", + &LexCmp_::alphabet, + R"pbdoc( +:sig=(self: LexCmp) -> Alphabet: +Return the alphabet used to compare letters. + +:returns: The stored alphabet. +:rtype: Alphabet + +:raises AttributeError: + if *self* was constructed without an alphabet. + +.. warning:: + This method only works if *self* was constructed as + ``LexCmp(alphabet)``. An object constructed as ``LexCmp()`` does not have a + stored alphabet. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, LexCmp + >>> alphabet = Alphabet("ba") + >>> LexCmp(alphabet).alphabet() == alphabet + True +)pbdoc", + py::return_value_policy::reference_internal); + } // bind_lex_cmp_with_alphabet + + template + void bind_lenlex_cmp_with_alphabet(py::module& m, char const* name) { + using LenLexCmp_ = LenLexCmp; + + // The Python wrapper copies all documentation from this + // specialization, except those given above. + py::class_ thing(m, name, R"pbdoc( +Compare words using len-lex ordering. + +Words are first compared by length and then lexicographically. Use +``LenLexCmp()`` to compare either ``str`` or ``list[int]`` words using the +natural order of their letters. Use ``LenLexCmp(alphabet)`` to compare words +of equal length by the positions of their letters in *alphabet*. The latter +form copies *alphabet* and only accepts words with the same type as *alphabet*. + +.. note:: + The constructor fixes whether this object is alphabet-aware (i.e. constructed + from an :any:`Alphabet` object). It also fixes the word type of an + alphabet-aware object. In particular, ``LenLexCmp()`` only accepts + ``init()`` (which does nothing); it cannot be changed into an alphabet-aware + object by calling ``init(alphabet)``. Similarly, + ``LenLexCmp(alphabet).init(new_alphabet)`` requires *new_alphabet* to have + the same word type as *alphabet*. + +.. seealso:: + + :any:`Alphabet` + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, LenLexCmp + >>> LenLexCmp()("a", "b") + True + >>> LenLexCmp()([0], [1]) + True + >>> LenLexCmp(Alphabet("ba"))("b", "a") + True +)pbdoc"); + + thing.def(py::init const&>(), + py::arg("alphabet"), + R"pbdoc( +:sig=(self: LenLexCmp, alphabet: Alphabet) -> None: +Construct a len-lex comparison object from an alphabet. + +Constructs an object whose call operator first compares words by length and +then compares words of equal length by the positions of their letters in +*alphabet*. The type of letters in *alphabet* also fixes the accepted word +type for the call operator. + +:param alphabet: the optional alphabet defining the order of letters. +:type alphabet: Alphabet + +:raises TypeError: + if more than one argument is given or the argument is not an :any:`Alphabet`. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, LenLexCmp + >>> LenLexCmp(Alphabet("ba"))("b", "a") + True +)pbdoc"); + + thing.def("__repr__", [](LenLexCmp_ const& self) { + return to_human_readable_repr(self); + }); + + thing.def("__copy__", + [](LenLexCmp_ const& self) { return LenLexCmp_(self); }); + + thing.def( + "copy", + [](LenLexCmp_ const& self) { return LenLexCmp_(self); }, + R"pbdoc( +:sig=(self: LenLexCmp) -> LenLexCmp: +Copy a comparison object. + +The copy has the same mode and word type as *self*. For an alphabet-aware +object, the stored alphabet is also copied, so subsequently reinitializing one +comparison object does not affect the other. + +:returns: A copy of *self*. +:rtype: LenLexCmp + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, LenLexCmp + >>> LenLexCmp(Alphabet("ba")).copy()("b", "a") + True +)pbdoc"); + + thing.def( + "init", + [](LenLexCmp_& self, Alphabet const& alphabet) -> LenLexCmp_& { + return self.init(alphabet); + }, + py::arg("alphabet"), + R"pbdoc( +:sig=(self: LenLexCmp, alphabet: Alphabet) -> LenLexCmp: +Reinitialize the comparison object. + +If *self* was constructed using an :any:`Alphabet`, then +``init(new_alphabet)`` puts *self* back into the same state it would have been +had it been newly constructed from *alphabet*. + +:param alphabet: the replacement alphabet. +:type alphabet: Alphabet + +:returns: The first argument *self*. +:rtype: LenLexCmp + +:raises TypeError: + if the type of the words in the new alphabet is not the same as the existing + alphabet. +:raises AttributeError: + if *self* was constructed without an alphabet. + +.. warning:: + This method only works if *self* was constructed as + ``LenLexCmp(alphabet)``. An object constructed as ``LenLexCmp()`` does not have a + stored alphabet. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, LenLexCmp + >>> compare = LenLexCmp(Alphabet("ab")) + >>> compare.init(Alphabet("ba")) is compare + True + >>> compare("b", "a") + True +)pbdoc"); + + thing.def( + "__call__", + [](LenLexCmp_ const& self, Word const& x, Word const& y) { + return self(x, y); + }, + py::arg("x"), + py::arg("y"), + R"pbdoc( +:sig=(self: LenLexCmp, x: str | list[int], y: str | list[int]) -> bool: +Compare two words using len-lex ordering. + +Words are first compared by length and then lexicographically. If *self* was +constructed as ``LenLexCmp()``, then *x* and *y* must either both be strings or +both be lists of integers, and letters are compared using their natural order. +If *self* was constructed using an :any:`Alphabet`, then *x* and *y* must have +the same type of words as *alphabet*, and letters are compared by their +positions in the alphabet. + +:param x: the first word. +:type x: str | list[int] +:param y: the second word. +:type y: str | list[int] +:returns: Whether *x* is less than *y*. +:rtype: bool + +:raises TypeError: + if *x* and *y* are not both strings or both lists of integers, or if their + type does not match the alphabet used to construct *self*. +:raises LibsemigroupsError: + if *self* is alphabet-aware and either word contains a letter that does not + belong to its alphabet. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import LenLexCmp + >>> LenLexCmp()("z", "aa") + True + >>> LenLexCmp()([9], [0, 0]) + True +)pbdoc"); + + thing.def("alphabet", + &LenLexCmp_::alphabet, + R"pbdoc( +:sig=(self: LenLexCmp) -> Alphabet: +Return the alphabet used to compare letters. + +:returns: The stored alphabet. +:rtype: Alphabet + +:raises AttributeError: + if *self* was constructed without an alphabet. + +.. warning:: + This method only works if *self* was constructed as + ``LenLexCmp(alphabet)``. An object constructed as ``LenLexCmp()`` does not + have a stored alphabet. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, LenLexCmp + >>> alphabet = Alphabet("ba") + >>> LenLexCmp(alphabet).alphabet() == alphabet + True +)pbdoc", + py::return_value_policy::reference_internal); + } // bind_lenlex_cmp_with_alphabet + } // namespace void init_order(py::module& m) { @@ -1312,5 +1766,14 @@ respectively, in new code. bind_order_comparisons(m); bind_order_comparisons(m); + + bind_cmp_default>(m, "Lex"); + bind_cmp_default>(m, "LenLex"); + + bind_lex_cmp_with_alphabet(m, "LexCmpString"); + bind_lex_cmp_with_alphabet(m, "LexCmpWord"); + + bind_lenlex_cmp_with_alphabet(m, "LenLexCmpString"); + bind_lenlex_cmp_with_alphabet(m, "LenLexCmpWord"); } } // namespace libsemigroups diff --git a/tests/test_order.py b/tests/test_order.py index 1b2f1950..f836f8c3 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -6,10 +6,14 @@ """Tests for order.cpp.""" +from copy import copy + import pytest from libsemigroups_pybind11 import ( Alphabet, + LenLexCmp, + LexCmp, LibsemigroupsError, Order, len_wt_lex_cmp, @@ -32,6 +36,110 @@ ) +def test_lex_cmp_object_without_alphabet(): + """Check the stateless comparison object with both word types.""" + compare = LexCmp() + assert compare("a", "b") + assert compare([0], [1]) + assert not compare("b", "aa") + + +@pytest.mark.parametrize( + ("alphabet", "x", "y", "missing"), + [(Alphabet("ba"), "b", "a", "c"), (Alphabet([1, 0]), [1], [0], [2])], +) +def test_lex_cmp_object_with_alphabet(alphabet, x, y, missing): + """Check that the comparison object stores and uses its alphabet.""" + compare = LexCmp(alphabet=alphabet) + assert compare(x, y) + assert compare.alphabet() == alphabet + assert compare.init(alphabet) is compare + + with pytest.raises(LibsemigroupsError): + compare(x, missing) + + +def test_lex_cmp_object_rejects_bad_constructors(): + """Check that only zero arguments or one Alphabet are accepted.""" + with pytest.raises(TypeError): + LexCmp("abc") + with pytest.raises(TypeError): + LexCmp(Alphabet("abc"), Alphabet("abc")) + + +def test_lenlex_cmp_object_without_alphabet(): + """Check the stateless comparison object with both word types.""" + compare = LenLexCmp() + assert compare("a", "b") + assert compare([0], [1]) + assert not compare("bb", "a") + + +@pytest.mark.parametrize( + ("alphabet", "x", "y", "missing"), + [(Alphabet("ba"), "b", "a", "c"), (Alphabet([1, 0]), [1], [0], [2])], +) +def test_lenlex_cmp_object_with_alphabet(alphabet, x, y, missing): + """Check that the comparison object stores and uses its alphabet.""" + compare = LenLexCmp(alphabet=alphabet) + assert compare(x, y) + assert compare.alphabet() == alphabet + assert compare.init(alphabet) is compare + + with pytest.raises(LibsemigroupsError): + compare(x, missing) + + +def test_lenlex_cmp_object_rejects_bad_constructors(): + """Check that only zero arguments or one Alphabet are accepted.""" + with pytest.raises(TypeError): + LenLexCmp("abc") + with pytest.raises(TypeError): + LenLexCmp(Alphabet("abc"), Alphabet("abc")) + + +@pytest.mark.parametrize("comparison_type", [LexCmp, LenLexCmp]) +@pytest.mark.parametrize( + ("alphabet", "replacement", "x", "y"), + [ + (None, None, "a", "b"), + (Alphabet("ba"), Alphabet("ab"), "b", "a"), + (Alphabet([1, 0]), Alphabet([0, 1]), [1], [0]), + ], +) +def test_comparison_object_copy(comparison_type, alphabet, replacement, x, y): + """Check that both copy APIs make independent copies of the C++ object.""" + original = comparison_type() if alphabet is None else comparison_type(alphabet) + copies = ( + original.copy(), + original.__copy__(), # pylint: disable=unnecessary-dunder-call + copy(original), + ) + + for copied in copies: + assert isinstance(copied, comparison_type) + assert copied is not original + assert copied.py_template_params == original.py_template_params + assert copied(x, y) + if alphabet is not None: + assert copied.alphabet() == alphabet + + if replacement is not None: + original.init(replacement) + assert not original(x, y) + assert all(copied(x, y) for copied in copies) + + +@pytest.mark.parametrize( + ("comparison_type", "name"), [(LexCmp, "LexCmp"), (LenLexCmp, "LenLexCmp")] +) +def test_comparison_object_repr(comparison_type, name): + """Check human-readable representations with and without alphabets.""" + assert repr(comparison_type()) == f"<{name} object>" + assert repr(comparison_type(Alphabet("ba"))) == f'<{name} object over >' + assert repr(comparison_type(Alphabet([1, 0]))) == f"<{name} object over >" + + @pytest.mark.parametrize( ("compare", "x", "y"), [ From 283d91e5f8def83feb16ad9f54f2df575a385cdf Mon Sep 17 00:00:00 2001 From: Codex OpenAI Date: Mon, 17 Aug 2026 14:21:53 +0100 Subject: [PATCH 02/27] order: support RPOCmp --- docs/source/data-structures/order/index.rst | 3 +- docs/source/data-structures/order/rpo-cmp.rst | 34 +++ src/libsemigroups_pybind11/__init__.py | 14 +- src/libsemigroups_pybind11/order.py | 69 +++++- src/order.cpp | 211 ++++++++++++++++++ tests/test_order.py | 36 ++- 6 files changed, 351 insertions(+), 16 deletions(-) create mode 100644 docs/source/data-structures/order/rpo-cmp.rst diff --git a/docs/source/data-structures/order/index.rst b/docs/source/data-structures/order/index.rst index ecacac07..1619c26e 100644 --- a/docs/source/data-structures/order/index.rst +++ b/docs/source/data-structures/order/index.rst @@ -18,7 +18,7 @@ comparing words or strings with respect to certain reduction orderings. - :any:`Order` - :any:`Alphabet` -The class and functions in ``libsemigroups_pybind11`` for ordering words and +The classes and functions in ``libsemigroups_pybind11`` for ordering words and strings are described on the following pages: .. toctree:: @@ -26,4 +26,5 @@ strings are described on the following pages: lex-cmp lenlex-cmp + rpo-cmp functions diff --git a/docs/source/data-structures/order/rpo-cmp.rst b/docs/source/data-structures/order/rpo-cmp.rst new file mode 100644 index 00000000..adfd3a9b --- /dev/null +++ b/docs/source/data-structures/order/rpo-cmp.rst @@ -0,0 +1,34 @@ +.. + Copyright (c) 2026 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 RPOCmp class +================ + +.. autoclass:: RPOCmp + :doc-only: + +Contents +-------- + +.. autosummary:: + :signatures: short + + ~RPOCmp + RPOCmp.__call__ + RPOCmp.alphabet + RPOCmp.copy + RPOCmp.init + +Full API +-------- + +.. autoclass:: RPOCmp + :class-doc-from: init + :special-members: __call__ + :members: diff --git a/src/libsemigroups_pybind11/__init__.py b/src/libsemigroups_pybind11/__init__.py index 55198ba2..8d0a92a8 100644 --- a/src/libsemigroups_pybind11/__init__.py +++ b/src/libsemigroups_pybind11/__init__.py @@ -53,17 +53,10 @@ from .knuth_bendix import KnuthBendix from .konieczny import Konieczny from .matrix import Matrix, MatrixKind -from .order import LenLexCmp, LexCmp, wr_cmp +from .order import LenLexCmp, LexCmp, RPOCmp from .presentation import InversePresentation, Presentation from .schreier_sims import SchreierSims -from .sims import ( - MinimalRepOrc, - RepOrc, - Sims1, - Sims2, - SimsRefinerFaithful, - SimsRefinerIdeals, -) +from .sims import MinimalRepOrc, RepOrc, Sims1, Sims2, SimsRefinerFaithful, SimsRefinerIdeals from .stephen import Stephen from .to import to from .todd_coxeter import ToddCoxeter @@ -134,6 +127,7 @@ shortlex_compare, side, tril, + wr_cmp as _wr_cmp, wt_lenlex_cmp as _wt_lenlex_cmp, wt_lex_cmp as _wt_lex_cmp, ) @@ -155,6 +149,7 @@ rev_wt_lenlex_cmp = _wrap_cxx_free_fn(_rev_wt_lenlex_cmp) rev_wt_lex_cmp = _wrap_cxx_free_fn(_rev_wt_lex_cmp) rpo_cmp = _wrap_cxx_free_fn(_rpo_cmp) +wr_cmp = _wrap_cxx_free_fn(_wr_cmp) wt_lenlex_cmp = _wrap_cxx_free_fn(_wt_lenlex_cmp) wt_lex_cmp = _wrap_cxx_free_fn(_wt_lex_cmp) @@ -177,6 +172,7 @@ "LimitMax", "LenLexCmp", "LexCmp", + "RPOCmp", "Meeter", "NegativeInfinity", "Order", diff --git a/src/libsemigroups_pybind11/order.py b/src/libsemigroups_pybind11/order.py index 449379ee..9ce2e558 100644 --- a/src/libsemigroups_pybind11/order.py +++ b/src/libsemigroups_pybind11/order.py @@ -15,7 +15,9 @@ LexCmpDefault as _LexCmpDefault, LexCmpString as _LexCmpString, LexCmpWord as _LexCmpWord, - wr_cmp as _wr_cmp, + RPOCmpDefault as _RPOCmpDefault, + RPOCmpString as _RPOCmpString, + RPOCmpWord as _RPOCmpWord, ) from .alphabet import Alphabet as _Alphabet @@ -24,7 +26,6 @@ copy_cxx_mem_fns as _copy_cxx_mem_fns, register_cxx_wrapped_type as _register_cxx_wrapped_type, to_cxx as _to_cxx, - wrap_cxx_free_fn as _wrap_cxx_free_fn, ) from .detail.decorators import copydoc as _copydoc @@ -149,11 +150,71 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) +class RPOCmp(_CxxWrapper): + __doc__ = _RPOCmpString.__doc__ + + _py_template_params_to_cxx_type = { + (): _RPOCmpDefault, + (str,): _RPOCmpString, + (list[int],): _RPOCmpWord, + } + + _cxx_type_to_py_template_params = dict( + zip( + _py_template_params_to_cxx_type.values(), + _py_template_params_to_cxx_type.keys(), + strict=True, + ) + ) + + _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} + + @_copydoc(_RPOCmpString.__init__, _RPOCmpDefault.__init__) + def __init__(self: _Self, *args, **kwargs) -> None: + super().__init__(*args, optional_kwargs=("alphabet",), **kwargs) + if _to_cxx(self) is not None: + return + + if len(args) > 1: + raise TypeError( + f"expected at most 1 positional argument, found {len(args)}" + ) + + if len(args) == 1 and kwargs: + raise TypeError( + 'expected either 1 positional argument or the keyword argument "alphabet", ' + "but found both" + ) + + if len(args) == 1: + alphabet = args[0] + else: + alphabet = kwargs.get("alphabet") + + if len(args) == 0 and "alphabet" not in kwargs: + self.py_template_params = () + self.init_cxx_obj() + elif isinstance(alphabet, _Alphabet): + self.py_template_params = alphabet.py_template_params + self.init_cxx_obj(alphabet) + else: + raise TypeError( + f"expected the argument to be an Alphabet, but found {type(alphabet)}" + ) + + @_copydoc(_RPOCmpString.__call__) + def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: + return super().__call__(x, y) + + _copy_cxx_mem_fns(_LenLexCmpString, LenLexCmp) _register_cxx_wrapped_type(_LenLexCmpDefault, LenLexCmp) _register_cxx_wrapped_type(_LenLexCmpString, LenLexCmp) _register_cxx_wrapped_type(_LenLexCmpWord, LenLexCmp) -wr_cmp = _wrap_cxx_free_fn(_wr_cmp) +_copy_cxx_mem_fns(_RPOCmpString, RPOCmp) +_register_cxx_wrapped_type(_RPOCmpDefault, RPOCmp) +_register_cxx_wrapped_type(_RPOCmpString, RPOCmp) +_register_cxx_wrapped_type(_RPOCmpWord, RPOCmp) -__all__ = ["LenLexCmp", "LexCmp", "wr_cmp"] +__all__ = ["LenLexCmp", "LexCmp", "RPOCmp"] diff --git a/src/order.cpp b/src/order.cpp index 16339a44..4572bb7e 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -183,6 +183,7 @@ compared by their positions in *alphabet*. belong to *alphabet*. .. doctest:: python + >>> from libsemigroups_pybind11 import Alphabet, lenlex_cmp >>> alphabet = Alphabet("ba") >>> lenlex_cmp(alphabet, "b", "a") @@ -866,6 +867,8 @@ Compare two words using recursive-path ordering. This function has significantly worse performance than :any:`lenlex_cmp` and :any:`lex_cmp`. +.. seealso:: :any:`RPOCmp` for a reusable recursive-path comparison object. + .. doctest:: python >>> from libsemigroups_pybind11 import rpo_cmp @@ -1684,6 +1687,210 @@ Return the alphabet used to compare letters. py::return_value_policy::reference_internal); } // bind_lenlex_cmp_with_alphabet + template + void bind_rpo_cmp_with_alphabet(py::module& m, char const* name) { + using RPOCmp_ = RPOCmp; + + // The Python wrapper copies all documentation from this specialization, + // except those given above. + py::class_ thing(m, name, R"pbdoc( +Compare words using recursive-path ordering. + +Use ``RPOCmp()`` to compare either ``str`` or ``list[int]`` words using the +natural order of their letters. Use ``RPOCmp(alphabet)`` to compare words by +the positions of their letters in *alphabet*. The latter form copies +*alphabet* and only accepts words with the same type as *alphabet*. + +.. note:: + The constructor fixes whether this object is alphabet-aware (i.e. constructed + from an :any:`Alphabet` object). It also fixes the word type of an + alphabet-aware object. In particular, ``RPOCmp()`` only accepts ``init()`` + (which does nothing); it cannot be changed into an alphabet-aware object by + calling ``init(alphabet)``. Similarly, + ``RPOCmp(alphabet).init(new_alphabet)`` requires *new_alphabet* to have the + same word type as *alphabet*. + +.. warning:: + This comparison has significantly worse performance than :any:`LenLexCmp` + and :any:`LexCmp`. + +.. seealso:: + + :any:`Alphabet` + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RPOCmp + >>> RPOCmp()("a", "b") + True + >>> RPOCmp()([0], [1]) + True + >>> RPOCmp(Alphabet("ba"))("b", "a") + True +)pbdoc"); + + thing.def(py::init const&>(), + py::arg("alphabet"), + R"pbdoc( +:sig=(self: RPOCmp, alphabet: Alphabet) -> None: +Construct a recursive-path comparison object from an alphabet. + +Constructs an object whose call operator compares words using recursive-path +ordering and orders letters by their positions in *alphabet*. The type of +letters in *alphabet* also fixes the accepted word type for the call operator. + +:param alphabet: the optional alphabet defining the order of letters. +:type alphabet: Alphabet + +:raises TypeError: + if more than one argument is given or the argument is not an :any:`Alphabet`. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RPOCmp + >>> RPOCmp(Alphabet("ba"))("b", "a") + True +)pbdoc"); + + thing.def("__repr__", [](RPOCmp_ const& self) { + return to_human_readable_repr(self); + }); + + thing.def("__copy__", [](RPOCmp_ const& self) { return RPOCmp_(self); }); + + thing.def( + "copy", + [](RPOCmp_ const& self) { return RPOCmp_(self); }, + R"pbdoc( +:sig=(self: RPOCmp) -> RPOCmp: +Copy a comparison object. + +The copy has the same mode and word type as *self*. For an alphabet-aware +object, the stored alphabet is also copied, so subsequently reinitializing one +comparison object does not affect the other. + +:returns: A copy of *self*. +:rtype: RPOCmp + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RPOCmp + >>> RPOCmp(Alphabet("ba")).copy()("b", "a") + True +)pbdoc"); + + thing.def( + "init", + [](RPOCmp_& self, Alphabet const& alphabet) -> RPOCmp_& { + return self.init(alphabet); + }, + py::arg("alphabet"), + R"pbdoc( +:sig=(self: RPOCmp, alphabet: Alphabet) -> RPOCmp: +Reinitialize the comparison object. + +If *self* was constructed using an :any:`Alphabet`, then +``init(new_alphabet)`` puts *self* back into the same state it would have been +had it been newly constructed from *alphabet*. + +:param alphabet: the replacement alphabet. +:type alphabet: Alphabet + +:returns: The first argument *self*. +:rtype: RPOCmp + +:raises TypeError: + if the type of the words in the new alphabet is not the same as the existing + alphabet. +:raises AttributeError: + if *self* was constructed without an alphabet. + +.. warning:: + This method only works if *self* was constructed as + ``RPOCmp(alphabet)``. An object constructed as ``RPOCmp()`` does not have a + stored alphabet. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RPOCmp + >>> compare = RPOCmp(Alphabet("ab")) + >>> compare.init(Alphabet("ba")) is compare + True + >>> compare("b", "a") + True +)pbdoc"); + + thing.def( + "__call__", + [](RPOCmp_ const& self, Word const& x, Word const& y) { + return self(x, y); + }, + py::arg("x"), + py::arg("y"), + R"pbdoc( +:sig=(self: RPOCmp, x: str | list[int], y: str | list[int]) -> bool: +Compare two words using recursive-path ordering. + +If *self* was constructed as ``RPOCmp()``, then *x* and *y* must either both +be strings or both be lists of integers, and letters are compared using their +natural order. If *self* was constructed using an :any:`Alphabet`, then *x* and +*y* must have the same type of words as *alphabet*, and letters are compared by +their positions in the alphabet. + +:param x: the first word. +:type x: str | list[int] +:param y: the second word. +:type y: str | list[int] +:returns: Whether *x* is less than *y*. +:rtype: bool + +:raises TypeError: + if *x* and *y* are not both strings or both lists of integers, or if their + type does not match the alphabet used to construct *self*. +:raises LibsemigroupsError: + if *self* is alphabet-aware and either word contains a letter that does not + belong to its alphabet. + +.. warning:: + This comparison has significantly worse performance than :any:`LenLexCmp` + and :any:`LexCmp`. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import RPOCmp + >>> RPOCmp()("a", "b") + True + >>> RPOCmp()([0], [1]) + True +)pbdoc"); + + thing.def("alphabet", + &RPOCmp_::alphabet, + R"pbdoc( +:sig=(self: RPOCmp) -> Alphabet: +Return the alphabet used to compare letters. + +:returns: The stored alphabet. +:rtype: Alphabet + +:raises AttributeError: + if *self* was constructed without an alphabet. + +.. warning:: + This method only works if *self* was constructed as + ``RPOCmp(alphabet)``. An object constructed as ``RPOCmp()`` does not have a + stored alphabet. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RPOCmp + >>> alphabet = Alphabet("ba") + >>> RPOCmp(alphabet).alphabet() == alphabet + True +)pbdoc", + py::return_value_policy::reference_internal); + } // bind_rpo_cmp_with_alphabet + } // namespace void init_order(py::module& m) { @@ -1769,11 +1976,15 @@ respectively, in new code. bind_cmp_default>(m, "Lex"); bind_cmp_default>(m, "LenLex"); + bind_cmp_default>(m, "RPO"); bind_lex_cmp_with_alphabet(m, "LexCmpString"); bind_lex_cmp_with_alphabet(m, "LexCmpWord"); bind_lenlex_cmp_with_alphabet(m, "LenLexCmpString"); bind_lenlex_cmp_with_alphabet(m, "LenLexCmpWord"); + + bind_rpo_cmp_with_alphabet(m, "RPOCmpString"); + bind_rpo_cmp_with_alphabet(m, "RPOCmpWord"); } } // namespace libsemigroups diff --git a/tests/test_order.py b/tests/test_order.py index f836f8c3..be1b25f0 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -16,6 +16,7 @@ LexCmp, LibsemigroupsError, Order, + RPOCmp, len_wt_lex_cmp, lenlex_cmp, lex_cmp, @@ -98,7 +99,38 @@ def test_lenlex_cmp_object_rejects_bad_constructors(): LenLexCmp(Alphabet("abc"), Alphabet("abc")) -@pytest.mark.parametrize("comparison_type", [LexCmp, LenLexCmp]) +def test_rpo_cmp_object_without_alphabet(): + """Check the stateless comparison object with both word types.""" + compare = RPOCmp() + assert compare("a", "b") + assert compare([0], [1]) + assert not compare("ab", "ba") + + +@pytest.mark.parametrize( + ("alphabet", "x", "y", "missing"), + [(Alphabet("ba"), "b", "a", "c"), (Alphabet([1, 0]), [1], [0], [2])], +) +def test_rpo_cmp_object_with_alphabet(alphabet, x, y, missing): + """Check that the comparison object stores and uses its alphabet.""" + compare = RPOCmp(alphabet=alphabet) + assert compare(x, y) + assert compare.alphabet() == alphabet + assert compare.init(alphabet) is compare + + with pytest.raises(LibsemigroupsError): + compare(x, missing) + + +def test_rpo_cmp_object_rejects_bad_constructors(): + """Check that only zero arguments or one Alphabet are accepted.""" + with pytest.raises(TypeError): + RPOCmp("abc") + with pytest.raises(TypeError): + RPOCmp(Alphabet("abc"), Alphabet("abc")) + + +@pytest.mark.parametrize("comparison_type", [LexCmp, LenLexCmp, RPOCmp]) @pytest.mark.parametrize( ("alphabet", "replacement", "x", "y"), [ @@ -131,7 +163,7 @@ def test_comparison_object_copy(comparison_type, alphabet, replacement, x, y): @pytest.mark.parametrize( - ("comparison_type", "name"), [(LexCmp, "LexCmp"), (LenLexCmp, "LenLexCmp")] + ("comparison_type", "name"), [(LexCmp, "LexCmp"), (LenLexCmp, "LenLexCmp"), (RPOCmp, "RPOCmp")] ) def test_comparison_object_repr(comparison_type, name): """Check human-readable representations with and without alphabets.""" From 1d777997eb894a9b72c76caa79b26a3ef5d763b7 Mon Sep 17 00:00:00 2001 From: Codex OpenAI Date: Mon, 17 Aug 2026 14:34:41 +0100 Subject: [PATCH 03/27] order: support RevRPOCmp --- docs/source/data-structures/order/index.rst | 1 + .../data-structures/order/rev-rpo-cmp.rst | 34 +++ src/libsemigroups_pybind11/__init__.py | 3 +- src/libsemigroups_pybind11/order.py | 64 ++++- src/order.cpp | 218 ++++++++++++++++++ tests/test_order.py | 37 ++- 6 files changed, 353 insertions(+), 4 deletions(-) create mode 100644 docs/source/data-structures/order/rev-rpo-cmp.rst diff --git a/docs/source/data-structures/order/index.rst b/docs/source/data-structures/order/index.rst index 1619c26e..84fb91fd 100644 --- a/docs/source/data-structures/order/index.rst +++ b/docs/source/data-structures/order/index.rst @@ -27,4 +27,5 @@ strings are described on the following pages: lex-cmp lenlex-cmp rpo-cmp + rev-rpo-cmp functions diff --git a/docs/source/data-structures/order/rev-rpo-cmp.rst b/docs/source/data-structures/order/rev-rpo-cmp.rst new file mode 100644 index 00000000..c5408590 --- /dev/null +++ b/docs/source/data-structures/order/rev-rpo-cmp.rst @@ -0,0 +1,34 @@ +.. + Copyright (c) 2026 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 RevRPOCmp class +=================== + +.. autoclass:: RevRPOCmp + :doc-only: + +Contents +-------- + +.. autosummary:: + :signatures: short + + ~RevRPOCmp + RevRPOCmp.__call__ + RevRPOCmp.alphabet + RevRPOCmp.copy + RevRPOCmp.init + +Full API +-------- + +.. autoclass:: RevRPOCmp + :class-doc-from: init + :special-members: __call__ + :members: diff --git a/src/libsemigroups_pybind11/__init__.py b/src/libsemigroups_pybind11/__init__.py index 8d0a92a8..0f3a7c28 100644 --- a/src/libsemigroups_pybind11/__init__.py +++ b/src/libsemigroups_pybind11/__init__.py @@ -53,7 +53,7 @@ from .knuth_bendix import KnuthBendix from .konieczny import Konieczny from .matrix import Matrix, MatrixKind -from .order import LenLexCmp, LexCmp, RPOCmp +from .order import LenLexCmp, LexCmp, RevRPOCmp, RPOCmp from .presentation import InversePresentation, Presentation from .schreier_sims import SchreierSims from .sims import MinimalRepOrc, RepOrc, Sims1, Sims2, SimsRefinerFaithful, SimsRefinerIdeals @@ -173,6 +173,7 @@ "LenLexCmp", "LexCmp", "RPOCmp", + "RevRPOCmp", "Meeter", "NegativeInfinity", "Order", diff --git a/src/libsemigroups_pybind11/order.py b/src/libsemigroups_pybind11/order.py index 9ce2e558..1fd0bedb 100644 --- a/src/libsemigroups_pybind11/order.py +++ b/src/libsemigroups_pybind11/order.py @@ -15,6 +15,9 @@ LexCmpDefault as _LexCmpDefault, LexCmpString as _LexCmpString, LexCmpWord as _LexCmpWord, + RevRPOCmpDefault as _RevRPOCmpDefault, + RevRPOCmpString as _RevRPOCmpString, + RevRPOCmpWord as _RevRPOCmpWord, RPOCmpDefault as _RPOCmpDefault, RPOCmpString as _RPOCmpString, RPOCmpWord as _RPOCmpWord, @@ -217,4 +220,63 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: _register_cxx_wrapped_type(_RPOCmpString, RPOCmp) _register_cxx_wrapped_type(_RPOCmpWord, RPOCmp) -__all__ = ["LenLexCmp", "LexCmp", "RPOCmp"] + +class RevRPOCmp(_CxxWrapper): + __doc__ = _RevRPOCmpString.__doc__ + + _py_template_params_to_cxx_type = { + (): _RevRPOCmpDefault, + (str,): _RevRPOCmpString, + (list[int],): _RevRPOCmpWord, + } + + _cxx_type_to_py_template_params = dict( + zip( + _py_template_params_to_cxx_type.values(), + _py_template_params_to_cxx_type.keys(), + strict=True, + ) + ) + + _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} + + @_copydoc(_RevRPOCmpString.__init__, _RevRPOCmpDefault.__init__) + def __init__(self: _Self, *args, **kwargs) -> None: + super().__init__(*args, optional_kwargs=("alphabet",), **kwargs) + if _to_cxx(self) is not None: + return + + if len(args) > 1: + raise TypeError(f"expected at most 1 positional argument, found {len(args)}") + + if len(args) == 1 and kwargs: + raise TypeError( + 'expected either 1 positional argument or the keyword argument "alphabet", ' + "but found both" + ) + + if len(args) == 1: + alphabet = args[0] + else: + alphabet = kwargs.get("alphabet") + + if len(args) == 0 and "alphabet" not in kwargs: + self.py_template_params = () + self.init_cxx_obj() + elif isinstance(alphabet, _Alphabet): + self.py_template_params = alphabet.py_template_params + self.init_cxx_obj(alphabet) + else: + raise TypeError(f"expected the argument to be an Alphabet, but found {type(alphabet)}") + + @_copydoc(_RevRPOCmpString.__call__) + def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: + return super().__call__(x, y) + + +_copy_cxx_mem_fns(_RevRPOCmpString, RevRPOCmp) +_register_cxx_wrapped_type(_RevRPOCmpDefault, RevRPOCmp) +_register_cxx_wrapped_type(_RevRPOCmpString, RevRPOCmp) +_register_cxx_wrapped_type(_RevRPOCmpWord, RevRPOCmp) + +__all__ = ["LenLexCmp", "LexCmp", "RPOCmp", "RevRPOCmp"] diff --git a/src/order.cpp b/src/order.cpp index 4572bb7e..b903ad87 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -940,6 +940,9 @@ left. This function has significantly worse performance than :any:`lenlex_cmp` and :any:`lex_cmp`. +.. seealso:: + :any:`RevRPOCmp` for a reusable reversed recursive-path comparison object. + .. doctest:: python >>> from libsemigroups_pybind11 import rev_rpo_cmp @@ -980,6 +983,9 @@ left, with letters compared by their positions in *alphabet*. This function has significantly worse performance than :any:`lenlex_cmp` and :any:`lex_cmp`. +.. seealso:: + :any:`RevRPOCmp` for a reusable reversed recursive-path comparison object. + .. doctest:: python >>> from libsemigroups_pybind11 import Alphabet, rev_rpo_cmp @@ -1891,6 +1897,214 @@ Return the alphabet used to compare letters. py::return_value_policy::reference_internal); } // bind_rpo_cmp_with_alphabet + template + void bind_rev_rpo_cmp_with_alphabet(py::module& m, char const* name) { + using RevRPOCmp_ = RevRPOCmp; + + // The Python wrapper copies all documentation from this specialization, + // except those given above. + py::class_ thing(m, name, R"pbdoc( +Compare words using reversed recursive-path ordering. + +This is recursive-path ordering applied after reading both words from right to +left. Use ``RevRPOCmp()`` to compare either ``str`` or ``list[int]`` words +using the natural order of their letters. Use ``RevRPOCmp(alphabet)`` to +compare words by the positions of their letters in *alphabet*. The latter form +copies *alphabet* and only accepts words with the same type as *alphabet*. + +.. note:: + The constructor fixes whether this object is alphabet-aware (i.e. constructed + from an :any:`Alphabet` object). It also fixes the word type of an + alphabet-aware object. In particular, ``RevRPOCmp()`` only accepts + ``init()`` (which does nothing); it cannot be changed into an alphabet-aware + object by calling ``init(alphabet)``. Similarly, + ``RevRPOCmp(alphabet).init(new_alphabet)`` requires *new_alphabet* to have + the same word type as *alphabet*. + +.. warning:: + This comparison has significantly worse performance than :any:`LenLexCmp` + and :any:`LexCmp`. + +.. seealso:: + + :any:`Alphabet` + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RevRPOCmp + >>> RevRPOCmp()("ab", "ba") + True + >>> RevRPOCmp()([0, 1], [1, 0]) + True + >>> RevRPOCmp(Alphabet("ba"))("b", "a") + True +)pbdoc"); + + thing.def(py::init const&>(), + py::arg("alphabet"), + R"pbdoc( +:sig=(self: RevRPOCmp, alphabet: Alphabet) -> None: +Construct a reversed recursive-path comparison object from an alphabet. + +Constructs an object whose call operator compares words using reversed +recursive-path ordering and orders letters by their positions in *alphabet*. +The type of letters in *alphabet* also fixes the accepted word type for the +call operator. + +:param alphabet: the optional alphabet defining the order of letters. +:type alphabet: Alphabet + +:raises TypeError: + if more than one argument is given or the argument is not an :any:`Alphabet`. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RevRPOCmp + >>> RevRPOCmp(Alphabet("ba"))("b", "a") + True +)pbdoc"); + + thing.def("__repr__", [](RevRPOCmp_ const& self) { + return to_human_readable_repr(self); + }); + + thing.def("__copy__", + [](RevRPOCmp_ const& self) { return RevRPOCmp_(self); }); + + thing.def( + "copy", + [](RevRPOCmp_ const& self) { return RevRPOCmp_(self); }, + R"pbdoc( +:sig=(self: RevRPOCmp) -> RevRPOCmp: +Copy a comparison object. + +The copy has the same mode and word type as *self*. For an alphabet-aware +object, the stored alphabet is also copied, so subsequently reinitializing one +comparison object does not affect the other. + +:returns: A copy of *self*. +:rtype: RevRPOCmp + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RevRPOCmp + >>> RevRPOCmp(Alphabet("ba")).copy()("b", "a") + True +)pbdoc"); + + thing.def( + "init", + [](RevRPOCmp_& self, Alphabet const& alphabet) -> RevRPOCmp_& { + return self.init(alphabet); + }, + py::arg("alphabet"), + R"pbdoc( +:sig=(self: RevRPOCmp, alphabet: Alphabet) -> RevRPOCmp: +Reinitialize the comparison object. + +If *self* was constructed using an :any:`Alphabet`, then +``init(new_alphabet)`` puts *self* back into the same state it would have been +had it been newly constructed from *alphabet*. + +:param alphabet: the replacement alphabet. +:type alphabet: Alphabet + +:returns: The first argument *self*. +:rtype: RevRPOCmp + +:raises TypeError: + if the type of the words in the new alphabet is not the same as the existing + alphabet. +:raises AttributeError: + if *self* was constructed without an alphabet. + +.. warning:: + This method only works if *self* was constructed as + ``RevRPOCmp(alphabet)``. An object constructed as ``RevRPOCmp()`` does not + have a stored alphabet. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RevRPOCmp + >>> compare = RevRPOCmp(Alphabet("ab")) + >>> compare.init(Alphabet("ba")) is compare + True + >>> compare("b", "a") + True +)pbdoc"); + + thing.def( + "__call__", + [](RevRPOCmp_ const& self, Word const& x, Word const& y) { + return self(x, y); + }, + py::arg("x"), + py::arg("y"), + R"pbdoc( +:sig=(self: RevRPOCmp, x: str | list[int], y: str | list[int]) -> bool: +Compare two words using reversed recursive-path ordering. + +This is recursive-path ordering applied after reading both words from right to +left. If *self* was constructed as ``RevRPOCmp()``, then *x* and *y* must +either both be strings or both be lists of integers, and letters are compared +using their natural order. If *self* was constructed using an :any:`Alphabet`, +then *x* and *y* must have the same type of words as *alphabet*, and letters are +compared by their positions in the alphabet. + +:param x: the first word. +:type x: str | list[int] +:param y: the second word. +:type y: str | list[int] +:returns: Whether *x* is less than *y*. +:rtype: bool + +:raises TypeError: + if *x* and *y* are not both strings or both lists of integers, or if their + type does not match the alphabet used to construct *self*. +:raises LibsemigroupsError: + if *self* is alphabet-aware and either word contains a letter that does not + belong to its alphabet. + +.. warning:: + This comparison has significantly worse performance than :any:`LenLexCmp` + and :any:`LexCmp`. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import RevRPOCmp + >>> RevRPOCmp()("ab", "ba") + True + >>> RevRPOCmp()([0, 1], [1, 0]) + True +)pbdoc"); + + thing.def("alphabet", + &RevRPOCmp_::alphabet, + R"pbdoc( +:sig=(self: RevRPOCmp) -> Alphabet: +Return the alphabet used to compare letters. + +:returns: The stored alphabet. +:rtype: Alphabet + +:raises AttributeError: + if *self* was constructed without an alphabet. + +.. warning:: + This method only works if *self* was constructed as + ``RevRPOCmp(alphabet)``. An object constructed as ``RevRPOCmp()`` does not + have a stored alphabet. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RevRPOCmp + >>> alphabet = Alphabet("ba") + >>> RevRPOCmp(alphabet).alphabet() == alphabet + True +)pbdoc", + py::return_value_policy::reference_internal); + } // bind_rev_rpo_cmp_with_alphabet + } // namespace void init_order(py::module& m) { @@ -1977,6 +2191,7 @@ respectively, in new code. bind_cmp_default>(m, "Lex"); bind_cmp_default>(m, "LenLex"); bind_cmp_default>(m, "RPO"); + bind_cmp_default>(m, "RevRPO"); bind_lex_cmp_with_alphabet(m, "LexCmpString"); bind_lex_cmp_with_alphabet(m, "LexCmpWord"); @@ -1986,5 +2201,8 @@ respectively, in new code. bind_rpo_cmp_with_alphabet(m, "RPOCmpString"); bind_rpo_cmp_with_alphabet(m, "RPOCmpWord"); + + bind_rev_rpo_cmp_with_alphabet(m, "RevRPOCmpString"); + bind_rev_rpo_cmp_with_alphabet(m, "RevRPOCmpWord"); } } // namespace libsemigroups diff --git a/tests/test_order.py b/tests/test_order.py index be1b25f0..543cca43 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -16,6 +16,7 @@ LexCmp, LibsemigroupsError, Order, + RevRPOCmp, RPOCmp, len_wt_lex_cmp, lenlex_cmp, @@ -130,7 +131,38 @@ def test_rpo_cmp_object_rejects_bad_constructors(): RPOCmp(Alphabet("abc"), Alphabet("abc")) -@pytest.mark.parametrize("comparison_type", [LexCmp, LenLexCmp, RPOCmp]) +def test_rev_rpo_cmp_object_without_alphabet(): + """Check the stateless comparison object with both word types.""" + compare = RevRPOCmp() + assert compare("a", "b") + assert compare([0], [1]) + assert not compare("ba", "ab") + + +@pytest.mark.parametrize( + ("alphabet", "x", "y", "missing"), + [(Alphabet("ba"), "b", "a", "c"), (Alphabet([1, 0]), [1], [0], [2])], +) +def test_rev_rpo_cmp_object_with_alphabet(alphabet, x, y, missing): + """Check that the comparison object stores and uses its alphabet.""" + compare = RevRPOCmp(alphabet=alphabet) + assert compare(x, y) + assert compare.alphabet() == alphabet + assert compare.init(alphabet) is compare + + with pytest.raises(LibsemigroupsError): + compare(x, missing) + + +def test_rev_rpo_cmp_object_rejects_bad_constructors(): + """Check that only zero arguments or one Alphabet are accepted.""" + with pytest.raises(TypeError): + RevRPOCmp("abc") + with pytest.raises(TypeError): + RevRPOCmp(Alphabet("abc"), Alphabet("abc")) + + +@pytest.mark.parametrize("comparison_type", [LexCmp, LenLexCmp, RPOCmp, RevRPOCmp]) @pytest.mark.parametrize( ("alphabet", "replacement", "x", "y"), [ @@ -163,7 +195,8 @@ def test_comparison_object_copy(comparison_type, alphabet, replacement, x, y): @pytest.mark.parametrize( - ("comparison_type", "name"), [(LexCmp, "LexCmp"), (LenLexCmp, "LenLexCmp"), (RPOCmp, "RPOCmp")] + ("comparison_type", "name"), + [(LexCmp, "LexCmp"), (LenLexCmp, "LenLexCmp"), (RPOCmp, "RPOCmp"), (RevRPOCmp, "RevRPOCmp")], ) def test_comparison_object_repr(comparison_type, name): """Check human-readable representations with and without alphabets.""" From f7c892caae58c9ce370d934bb7a7d9bafef2512d Mon Sep 17 00:00:00 2001 From: CodexAI Date: Mon, 17 Aug 2026 14:41:50 +0100 Subject: [PATCH 04/27] order: support RevLexCmp --- docs/source/data-structures/order/index.rst | 1 + .../data-structures/order/rev-lex-cmp.rst | 34 +++ src/libsemigroups_pybind11/__init__.py | 3 +- src/libsemigroups_pybind11/order.py | 64 +++++- src/order.cpp | 206 ++++++++++++++++++ tests/test_order.py | 42 +++- 6 files changed, 346 insertions(+), 4 deletions(-) create mode 100644 docs/source/data-structures/order/rev-lex-cmp.rst diff --git a/docs/source/data-structures/order/index.rst b/docs/source/data-structures/order/index.rst index 84fb91fd..c1274e60 100644 --- a/docs/source/data-structures/order/index.rst +++ b/docs/source/data-structures/order/index.rst @@ -25,6 +25,7 @@ strings are described on the following pages: :maxdepth: 1 lex-cmp + rev-lex-cmp lenlex-cmp rpo-cmp rev-rpo-cmp diff --git a/docs/source/data-structures/order/rev-lex-cmp.rst b/docs/source/data-structures/order/rev-lex-cmp.rst new file mode 100644 index 00000000..f89f5362 --- /dev/null +++ b/docs/source/data-structures/order/rev-lex-cmp.rst @@ -0,0 +1,34 @@ +.. + Copyright (c) 2026 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 RevLexCmp class +=================== + +.. autoclass:: RevLexCmp + :doc-only: + +Contents +-------- + +.. autosummary:: + :signatures: short + + ~RevLexCmp + RevLexCmp.__call__ + RevLexCmp.alphabet + RevLexCmp.copy + RevLexCmp.init + +Full API +-------- + +.. autoclass:: RevLexCmp + :class-doc-from: init + :special-members: __call__ + :members: diff --git a/src/libsemigroups_pybind11/__init__.py b/src/libsemigroups_pybind11/__init__.py index 0f3a7c28..1a19ba68 100644 --- a/src/libsemigroups_pybind11/__init__.py +++ b/src/libsemigroups_pybind11/__init__.py @@ -53,7 +53,7 @@ from .knuth_bendix import KnuthBendix from .konieczny import Konieczny from .matrix import Matrix, MatrixKind -from .order import LenLexCmp, LexCmp, RevRPOCmp, RPOCmp +from .order import LenLexCmp, LexCmp, RevLexCmp, RevRPOCmp, RPOCmp from .presentation import InversePresentation, Presentation from .schreier_sims import SchreierSims from .sims import MinimalRepOrc, RepOrc, Sims1, Sims2, SimsRefinerFaithful, SimsRefinerIdeals @@ -173,6 +173,7 @@ "LenLexCmp", "LexCmp", "RPOCmp", + "RevLexCmp", "RevRPOCmp", "Meeter", "NegativeInfinity", diff --git a/src/libsemigroups_pybind11/order.py b/src/libsemigroups_pybind11/order.py index 1fd0bedb..edc8387a 100644 --- a/src/libsemigroups_pybind11/order.py +++ b/src/libsemigroups_pybind11/order.py @@ -15,6 +15,9 @@ LexCmpDefault as _LexCmpDefault, LexCmpString as _LexCmpString, LexCmpWord as _LexCmpWord, + RevLexCmpDefault as _RevLexCmpDefault, + RevLexCmpString as _RevLexCmpString, + RevLexCmpWord as _RevLexCmpWord, RevRPOCmpDefault as _RevRPOCmpDefault, RevRPOCmpString as _RevRPOCmpString, RevRPOCmpWord as _RevRPOCmpWord, @@ -96,6 +99,65 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: _register_cxx_wrapped_type(_LexCmpWord, LexCmp) +class RevLexCmp(_CxxWrapper): + __doc__ = _RevLexCmpString.__doc__ + + _py_template_params_to_cxx_type = { + (): _RevLexCmpDefault, + (str,): _RevLexCmpString, + (list[int],): _RevLexCmpWord, + } + + _cxx_type_to_py_template_params = dict( + zip( + _py_template_params_to_cxx_type.values(), + _py_template_params_to_cxx_type.keys(), + strict=True, + ) + ) + + _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} + + @_copydoc(_RevLexCmpString.__init__, _RevLexCmpDefault.__init__) + def __init__(self: _Self, *args, **kwargs) -> None: + super().__init__(*args, optional_kwargs=("alphabet",), **kwargs) + if _to_cxx(self) is not None: + return + + if len(args) > 1: + raise TypeError(f"expected at most 1 positional argument, found {len(args)}") + + if len(args) == 1 and kwargs: + raise TypeError( + 'expected either 1 positional argument or the keyword argument "alphabet", ' + "but found both" + ) + + if len(args) == 1: + alphabet = args[0] + else: + alphabet = kwargs.get("alphabet") + + if len(args) == 0 and "alphabet" not in kwargs: + self.py_template_params = () + self.init_cxx_obj() + elif isinstance(alphabet, _Alphabet): + self.py_template_params = alphabet.py_template_params + self.init_cxx_obj(alphabet) + else: + raise TypeError(f"expected the argument to be an Alphabet, but found {type(alphabet)}") + + @_copydoc(_RevLexCmpString.__call__) + def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: + return super().__call__(x, y) + + +_copy_cxx_mem_fns(_RevLexCmpString, RevLexCmp) +_register_cxx_wrapped_type(_RevLexCmpDefault, RevLexCmp) +_register_cxx_wrapped_type(_RevLexCmpString, RevLexCmp) +_register_cxx_wrapped_type(_RevLexCmpWord, RevLexCmp) + + class LenLexCmp(_CxxWrapper): __doc__ = _LenLexCmpString.__doc__ @@ -279,4 +341,4 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: _register_cxx_wrapped_type(_RevRPOCmpString, RevRPOCmp) _register_cxx_wrapped_type(_RevRPOCmpWord, RevRPOCmp) -__all__ = ["LenLexCmp", "LexCmp", "RPOCmp", "RevRPOCmp"] +__all__ = ["LenLexCmp", "LexCmp", "RPOCmp", "RevLexCmp", "RevRPOCmp"] diff --git a/src/order.cpp b/src/order.cpp index b903ad87..c4738e1f 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -2105,6 +2105,208 @@ Return the alphabet used to compare letters. py::return_value_policy::reference_internal); } // bind_rev_rpo_cmp_with_alphabet + template + void bind_rev_lex_cmp_with_alphabet(py::module& m, char const* name) { + using RevLexCmp_ = RevLexCmp; + + // The Python wrapper copies all documentation from this specialization, + // except those given above. + py::class_ thing(m, name, R"pbdoc( +Compare words using reversed lexicographic ordering. + +This is lexicographic ordering applied after reading both words from right to +left. + +Use ``RevLexCmp()`` to compare either ``str`` or ``list[int]`` words using the +natural order of their letters. Use ``RevLexCmp(alphabet)`` to compare words by +the positions of their letters in *alphabet*. The latter form copies +*alphabet* and only accepts words with the same type as *alphabet*. + +.. note:: + The constructor fixes whether this object is alphabet-aware (i.e. constructed + from an :any:`Alphabet` object). It also fixes the word type of an + alphabet-aware object. In particular, ``RevLexCmp()`` only accepts ``init()`` + (which does nothing); it cannot be changed into an alphabet-aware object by + calling ``init(alphabet)``. Similarly, + ``RevLexCmp(alphabet).init(new_alphabet)`` requires *new_alphabet* to have the + same word type as *alphabet*. + +.. seealso:: + + :any:`Alphabet` + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RevLexCmp + >>> RevLexCmp()("a", "b") + True + >>> RevLexCmp()([0], [1]) + True + >>> RevLexCmp(Alphabet("ba"))("b", "a") + True +)pbdoc"); + + thing.def(py::init const&>(), + py::arg("alphabet"), + R"pbdoc( +:sig=(self: RevLexCmp, alphabet: Alphabet) -> None: +Construct a reversed lexicographic comparison object from an alphabet. + +Constructs an object whose call operator compares words by their positions in +*alphabet*. The type of letters in *alphabet* also fixes the accepted word type +for the call operator. + +:param alphabet: the optional alphabet defining the order of letters. +:type alphabet: Alphabet + +:raises TypeError: + if more than one argument is given or the argument is not an :any:`Alphabet`. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RevLexCmp + >>> RevLexCmp(Alphabet("ba"))("b", "a") + True +)pbdoc"); + + thing.def("__repr__", [](RevLexCmp_ const& self) { + return to_human_readable_repr(self); + }); + + thing.def("__copy__", + [](RevLexCmp_ const& self) { return RevLexCmp_(self); }); + + thing.def( + "copy", + [](RevLexCmp_ const& self) { return RevLexCmp_(self); }, + R"pbdoc( +:sig=(self: RevLexCmp) -> RevLexCmp: +Copy a comparison object. + +The copy has the same mode and word type as *self*. For an alphabet-aware +object, the stored alphabet is also copied, so subsequently reinitializing one +comparison object does not affect the other. + +:returns: A copy of *self*. +:rtype: RevLexCmp + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RevLexCmp + >>> RevLexCmp(Alphabet("ba")).copy()("b", "a") + True +)pbdoc"); + + thing.def( + "init", + [](RevLexCmp_& self, Alphabet const& alphabet) -> RevLexCmp_& { + return self.init(alphabet); + }, + py::arg("alphabet"), + R"pbdoc( +:sig=(self: RevLexCmp, alphabet: Alphabet) -> RevLexCmp: +Reinitialize the comparison object. + +If *self* was constructed using an :any:`Alphabet`, then ``init(new_alphabet)`` +puts *self* back into the same state it would have been had it been newly +constructed from *alphabet*. + +:param alphabet: the replacement alphabet. +:type alphabet: Alphabet + +:returns: The first argument *self*. +:rtype: RevLexCmp + +:raises TypeError: + if the type of the words in the new alphabet is not the same as the existing alphabet. +:raises AttributeError: + if *self* was constructed without an alphabet. + +.. warning:: + This method only works if *self* was constructed as + ``RevLexCmp(alphabet)``. An object constructed as ``RevLexCmp()`` does not have a + stored alphabet. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RevLexCmp + >>> compare = RevLexCmp(Alphabet("ab")) + >>> compare.init(Alphabet("ba")) is compare + True + >>> compare("b", "a") + True +)pbdoc"); + + thing.def( + "__call__", + [](RevLexCmp_ const& self, Word const& x, Word const& y) { + return self(x, y); + }, + py::arg("x"), + py::arg("y"), + R"pbdoc( +:sig=(self: RevLexCmp, x: str | list[int], y: str | list[int]) -> bool: +Compare two words using reversed lexicographic ordering. + +This is lexicographic ordering applied after reading both words from right to +left. + +If *self* was constructed as ``RevLexCmp()``, then *x* and *y* must either both +be strings or both be lists of integers, and letters are compared using their +natural order. If *self* was constructed using an :any:`Alphabet`, then *x* and +*y* must have the same type of words as *alphabet*, and letters are compared by +their positions in the alphabet. + +:param x: the first word. +:type x: str | list[int] +:param y: the second word. +:type y: str | list[int] +:returns: Whether *x* is less than *y*. +:rtype: bool + +:raises TypeError: + if *x* and *y* are not both strings or both lists of integers, or if their + type does not match the alphabet used to construct *self*. +:raises LibsemigroupsError: + if *self* is alphabet-aware and either word contains a letter that does not + belong to its alphabet. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import RevLexCmp + >>> RevLexCmp()("ba", "ab") + True + >>> RevLexCmp()([1, 0], [0, 1]) + True +)pbdoc"); + + thing.def("alphabet", + &RevLexCmp_::alphabet, + R"pbdoc( +:sig=(self: RevLexCmp) -> Alphabet: +Return the alphabet used to compare letters. + +:returns: The stored alphabet. +:rtype: Alphabet + +:raises AttributeError: + if *self* was constructed without an alphabet. + +.. warning:: + This method only works if *self* was constructed as + ``RevLexCmp(alphabet)``. An object constructed as ``RevLexCmp()`` does not have a + stored alphabet. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RevLexCmp + >>> alphabet = Alphabet("ba") + >>> RevLexCmp(alphabet).alphabet() == alphabet + True +)pbdoc", + py::return_value_policy::reference_internal); + } // bind_rev_lex_cmp_with_alphabet + } // namespace void init_order(py::module& m) { @@ -2189,6 +2391,7 @@ respectively, in new code. bind_order_comparisons(m); bind_cmp_default>(m, "Lex"); + bind_cmp_default>(m, "RevLex"); bind_cmp_default>(m, "LenLex"); bind_cmp_default>(m, "RPO"); bind_cmp_default>(m, "RevRPO"); @@ -2196,6 +2399,9 @@ respectively, in new code. bind_lex_cmp_with_alphabet(m, "LexCmpString"); bind_lex_cmp_with_alphabet(m, "LexCmpWord"); + bind_rev_lex_cmp_with_alphabet(m, "RevLexCmpString"); + bind_rev_lex_cmp_with_alphabet(m, "RevLexCmpWord"); + bind_lenlex_cmp_with_alphabet(m, "LenLexCmpString"); bind_lenlex_cmp_with_alphabet(m, "LenLexCmpWord"); diff --git a/tests/test_order.py b/tests/test_order.py index 543cca43..b94969d2 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -16,6 +16,7 @@ LexCmp, LibsemigroupsError, Order, + RevLexCmp, RevRPOCmp, RPOCmp, len_wt_lex_cmp, @@ -69,6 +70,37 @@ def test_lex_cmp_object_rejects_bad_constructors(): LexCmp(Alphabet("abc"), Alphabet("abc")) +def test_rev_lex_cmp_object_without_alphabet(): + """Check the stateless comparison object with both word types.""" + compare = RevLexCmp() + assert compare("a", "b") + assert compare([0], [1]) + assert compare("ba", "ab") + + +@pytest.mark.parametrize( + ("alphabet", "x", "y", "missing"), + [(Alphabet("ba"), "b", "a", "c"), (Alphabet([1, 0]), [1], [0], [2])], +) +def test_rev_lex_cmp_object_with_alphabet(alphabet, x, y, missing): + """Check that the comparison object stores and uses its alphabet.""" + compare = RevLexCmp(alphabet=alphabet) + assert compare(x, y) + assert compare.alphabet() == alphabet + assert compare.init(alphabet) is compare + + with pytest.raises(LibsemigroupsError): + compare(x, missing) + + +def test_rev_lex_cmp_object_rejects_bad_constructors(): + """Check that only zero arguments or one Alphabet are accepted.""" + with pytest.raises(TypeError): + RevLexCmp("abc") + with pytest.raises(TypeError): + RevLexCmp(Alphabet("abc"), Alphabet("abc")) + + def test_lenlex_cmp_object_without_alphabet(): """Check the stateless comparison object with both word types.""" compare = LenLexCmp() @@ -162,7 +194,7 @@ def test_rev_rpo_cmp_object_rejects_bad_constructors(): RevRPOCmp(Alphabet("abc"), Alphabet("abc")) -@pytest.mark.parametrize("comparison_type", [LexCmp, LenLexCmp, RPOCmp, RevRPOCmp]) +@pytest.mark.parametrize("comparison_type", [LexCmp, RevLexCmp, LenLexCmp, RPOCmp, RevRPOCmp]) @pytest.mark.parametrize( ("alphabet", "replacement", "x", "y"), [ @@ -196,7 +228,13 @@ def test_comparison_object_copy(comparison_type, alphabet, replacement, x, y): @pytest.mark.parametrize( ("comparison_type", "name"), - [(LexCmp, "LexCmp"), (LenLexCmp, "LenLexCmp"), (RPOCmp, "RPOCmp"), (RevRPOCmp, "RevRPOCmp")], + [ + (LexCmp, "LexCmp"), + (RevLexCmp, "RevLexCmp"), + (LenLexCmp, "LenLexCmp"), + (RPOCmp, "RPOCmp"), + (RevRPOCmp, "RevRPOCmp"), + ], ) def test_comparison_object_repr(comparison_type, name): """Check human-readable representations with and without alphabets.""" From 2685ab6b9e64eef3b3eb19852adfe0d708a2de4d Mon Sep 17 00:00:00 2001 From: CodexAI Date: Mon, 17 Aug 2026 14:45:18 +0100 Subject: [PATCH 05/27] order: support RevLenLexCmp --- docs/source/data-structures/order/index.rst | 1 + .../data-structures/order/rev-lenlex-cmp.rst | 34 +++ src/libsemigroups_pybind11/__init__.py | 3 +- src/libsemigroups_pybind11/order.py | 128 +++++++---- src/order.cpp | 205 +++++++++++++++++- tests/test_order.py | 37 +++- 6 files changed, 364 insertions(+), 44 deletions(-) create mode 100644 docs/source/data-structures/order/rev-lenlex-cmp.rst diff --git a/docs/source/data-structures/order/index.rst b/docs/source/data-structures/order/index.rst index c1274e60..2f6fb19d 100644 --- a/docs/source/data-structures/order/index.rst +++ b/docs/source/data-structures/order/index.rst @@ -27,6 +27,7 @@ strings are described on the following pages: lex-cmp rev-lex-cmp lenlex-cmp + rev-lenlex-cmp rpo-cmp rev-rpo-cmp functions diff --git a/docs/source/data-structures/order/rev-lenlex-cmp.rst b/docs/source/data-structures/order/rev-lenlex-cmp.rst new file mode 100644 index 00000000..69deed3f --- /dev/null +++ b/docs/source/data-structures/order/rev-lenlex-cmp.rst @@ -0,0 +1,34 @@ +.. + Copyright (c) 2026 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 RevLenLexCmp class +====================== + +.. autoclass:: RevLenLexCmp + :doc-only: + +Contents +-------- + +.. autosummary:: + :signatures: short + + ~RevLenLexCmp + RevLenLexCmp.__call__ + RevLenLexCmp.alphabet + RevLenLexCmp.copy + RevLenLexCmp.init + +Full API +-------- + +.. autoclass:: RevLenLexCmp + :class-doc-from: init + :special-members: __call__ + :members: diff --git a/src/libsemigroups_pybind11/__init__.py b/src/libsemigroups_pybind11/__init__.py index 1a19ba68..77c6dd4e 100644 --- a/src/libsemigroups_pybind11/__init__.py +++ b/src/libsemigroups_pybind11/__init__.py @@ -53,7 +53,7 @@ from .knuth_bendix import KnuthBendix from .konieczny import Konieczny from .matrix import Matrix, MatrixKind -from .order import LenLexCmp, LexCmp, RevLexCmp, RevRPOCmp, RPOCmp +from .order import LenLexCmp, LexCmp, RevLenLexCmp, RevLexCmp, RevRPOCmp, RPOCmp from .presentation import InversePresentation, Presentation from .schreier_sims import SchreierSims from .sims import MinimalRepOrc, RepOrc, Sims1, Sims2, SimsRefinerFaithful, SimsRefinerIdeals @@ -173,6 +173,7 @@ "LenLexCmp", "LexCmp", "RPOCmp", + "RevLenLexCmp", "RevLexCmp", "RevRPOCmp", "Meeter", diff --git a/src/libsemigroups_pybind11/order.py b/src/libsemigroups_pybind11/order.py index edc8387a..c8f75c90 100644 --- a/src/libsemigroups_pybind11/order.py +++ b/src/libsemigroups_pybind11/order.py @@ -15,6 +15,9 @@ LexCmpDefault as _LexCmpDefault, LexCmpString as _LexCmpString, LexCmpWord as _LexCmpWord, + RevLenLexCmpDefault as _RevLenLexCmpDefault, + RevLenLexCmpString as _RevLenLexCmpString, + RevLenLexCmpWord as _RevLenLexCmpWord, RevLexCmpDefault as _RevLexCmpDefault, RevLexCmpString as _RevLexCmpString, RevLexCmpWord as _RevLexCmpWord, @@ -62,9 +65,7 @@ def __init__(self: _Self, *args, **kwargs) -> None: return if len(args) > 1: - raise TypeError( - f"expected at most 1 positional argument, found {len(args)}" - ) + raise TypeError(f"expected at most 1 positional argument, found {len(args)}") if len(args) == 1 and kwargs: raise TypeError( @@ -84,21 +85,13 @@ def __init__(self: _Self, *args, **kwargs) -> None: self.py_template_params = alphabet.py_template_params self.init_cxx_obj(alphabet) else: - raise TypeError( - f"expected the argument to be an Alphabet, but found {type(alphabet)}" - ) + raise TypeError(f"expected the argument to be an Alphabet, but found {type(alphabet)}") @_copydoc(_LexCmpString.__call__) def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) -_copy_cxx_mem_fns(_LexCmpString, LexCmp) -_register_cxx_wrapped_type(_LexCmpDefault, LexCmp) -_register_cxx_wrapped_type(_LexCmpString, LexCmp) -_register_cxx_wrapped_type(_LexCmpWord, LexCmp) - - class RevLexCmp(_CxxWrapper): __doc__ = _RevLexCmpString.__doc__ @@ -152,12 +145,6 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) -_copy_cxx_mem_fns(_RevLexCmpString, RevLexCmp) -_register_cxx_wrapped_type(_RevLexCmpDefault, RevLexCmp) -_register_cxx_wrapped_type(_RevLexCmpString, RevLexCmp) -_register_cxx_wrapped_type(_RevLexCmpWord, RevLexCmp) - - class LenLexCmp(_CxxWrapper): __doc__ = _LenLexCmpString.__doc__ @@ -184,9 +171,7 @@ def __init__(self: _Self, *args, **kwargs) -> None: return if len(args) > 1: - raise TypeError( - f"expected at most 1 positional argument, found {len(args)}" - ) + raise TypeError(f"expected at most 1 positional argument, found {len(args)}") if len(args) == 1 and kwargs: raise TypeError( @@ -206,11 +191,62 @@ def __init__(self: _Self, *args, **kwargs) -> None: self.py_template_params = alphabet.py_template_params self.init_cxx_obj(alphabet) else: + raise TypeError(f"expected the argument to be an Alphabet, but found {type(alphabet)}") + + @_copydoc(_LenLexCmpString.__call__) + def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: + return super().__call__(x, y) + + +class RevLenLexCmp(_CxxWrapper): + __doc__ = _RevLenLexCmpString.__doc__ + + _py_template_params_to_cxx_type = { + (): _RevLenLexCmpDefault, + (str,): _RevLenLexCmpString, + (list[int],): _RevLenLexCmpWord, + } + + _cxx_type_to_py_template_params = dict( + zip( + _py_template_params_to_cxx_type.values(), + _py_template_params_to_cxx_type.keys(), + strict=True, + ) + ) + + _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} + + @_copydoc(_RevLenLexCmpString.__init__, _RevLenLexCmpDefault.__init__) + def __init__(self: _Self, *args, **kwargs) -> None: + super().__init__(*args, optional_kwargs=("alphabet",), **kwargs) + if _to_cxx(self) is not None: + return + + if len(args) > 1: + raise TypeError(f"expected at most 1 positional argument, found {len(args)}") + + if len(args) == 1 and kwargs: raise TypeError( - f"expected the argument to be an Alphabet, but found {type(alphabet)}" + 'expected either 1 positional argument or the keyword argument "alphabet", ' + "but found both" ) - @_copydoc(_LenLexCmpString.__call__) + if len(args) == 1: + alphabet = args[0] + else: + alphabet = kwargs.get("alphabet") + + if len(args) == 0 and "alphabet" not in kwargs: + self.py_template_params = () + self.init_cxx_obj() + elif isinstance(alphabet, _Alphabet): + self.py_template_params = alphabet.py_template_params + self.init_cxx_obj(alphabet) + else: + raise TypeError(f"expected the argument to be an Alphabet, but found {type(alphabet)}") + + @_copydoc(_RevLenLexCmpString.__call__) def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) @@ -241,9 +277,7 @@ def __init__(self: _Self, *args, **kwargs) -> None: return if len(args) > 1: - raise TypeError( - f"expected at most 1 positional argument, found {len(args)}" - ) + raise TypeError(f"expected at most 1 positional argument, found {len(args)}") if len(args) == 1 and kwargs: raise TypeError( @@ -263,26 +297,13 @@ def __init__(self: _Self, *args, **kwargs) -> None: self.py_template_params = alphabet.py_template_params self.init_cxx_obj(alphabet) else: - raise TypeError( - f"expected the argument to be an Alphabet, but found {type(alphabet)}" - ) + raise TypeError(f"expected the argument to be an Alphabet, but found {type(alphabet)}") @_copydoc(_RPOCmpString.__call__) def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) -_copy_cxx_mem_fns(_LenLexCmpString, LenLexCmp) -_register_cxx_wrapped_type(_LenLexCmpDefault, LenLexCmp) -_register_cxx_wrapped_type(_LenLexCmpString, LenLexCmp) -_register_cxx_wrapped_type(_LenLexCmpWord, LenLexCmp) - -_copy_cxx_mem_fns(_RPOCmpString, RPOCmp) -_register_cxx_wrapped_type(_RPOCmpDefault, RPOCmp) -_register_cxx_wrapped_type(_RPOCmpString, RPOCmp) -_register_cxx_wrapped_type(_RPOCmpWord, RPOCmp) - - class RevRPOCmp(_CxxWrapper): __doc__ = _RevRPOCmpString.__doc__ @@ -336,9 +357,34 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) +_copy_cxx_mem_fns(_LenLexCmpString, LenLexCmp) +_register_cxx_wrapped_type(_LenLexCmpDefault, LenLexCmp) +_register_cxx_wrapped_type(_LenLexCmpString, LenLexCmp) +_register_cxx_wrapped_type(_LenLexCmpWord, LenLexCmp) + +_copy_cxx_mem_fns(_RevLenLexCmpString, RevLenLexCmp) +_register_cxx_wrapped_type(_RevLenLexCmpDefault, RevLenLexCmp) +_register_cxx_wrapped_type(_RevLenLexCmpString, RevLenLexCmp) +_register_cxx_wrapped_type(_RevLenLexCmpWord, RevLenLexCmp) + +_copy_cxx_mem_fns(_LexCmpString, LexCmp) +_register_cxx_wrapped_type(_LexCmpDefault, LexCmp) +_register_cxx_wrapped_type(_LexCmpString, LexCmp) +_register_cxx_wrapped_type(_LexCmpWord, LexCmp) + +_copy_cxx_mem_fns(_RevLexCmpString, RevLexCmp) +_register_cxx_wrapped_type(_RevLexCmpDefault, RevLexCmp) +_register_cxx_wrapped_type(_RevLexCmpString, RevLexCmp) +_register_cxx_wrapped_type(_RevLexCmpWord, RevLexCmp) + +_copy_cxx_mem_fns(_RPOCmpString, RPOCmp) +_register_cxx_wrapped_type(_RPOCmpDefault, RPOCmp) +_register_cxx_wrapped_type(_RPOCmpString, RPOCmp) +_register_cxx_wrapped_type(_RPOCmpWord, RPOCmp) + _copy_cxx_mem_fns(_RevRPOCmpString, RevRPOCmp) _register_cxx_wrapped_type(_RevRPOCmpDefault, RevRPOCmp) _register_cxx_wrapped_type(_RevRPOCmpString, RevRPOCmp) _register_cxx_wrapped_type(_RevRPOCmpWord, RevRPOCmp) -__all__ = ["LenLexCmp", "LexCmp", "RPOCmp", "RevLexCmp", "RevRPOCmp"] +__all__ = ["LenLexCmp", "LexCmp", "RPOCmp", "RevLenLexCmp", "RevLexCmp", "RevRPOCmp"] diff --git a/src/order.cpp b/src/order.cpp index c4738e1f..aa7cccfb 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -2307,7 +2307,206 @@ Return the alphabet used to compare letters. py::return_value_policy::reference_internal); } // bind_rev_lex_cmp_with_alphabet - } // namespace + template + void bind_rev_lenlex_cmp_with_alphabet(py::module& m, char const* name) { + using RevLenLexCmp_ = RevLenLexCmp; + + // The Python wrapper copies all documentation from this specialization, + // except those given above. + py::class_ thing(m, name, R"pbdoc( +Compare words using reversed len-lex ordering. + +Words are first compared by length and then lexicographically after being read +from right to left. Use ``RevLenLexCmp()`` to compare either ``str`` or +``list[int]`` words using the natural order of their letters. Use +``RevLenLexCmp(alphabet)`` to compare words of equal length by the positions of +their letters in *alphabet*. The latter form copies *alphabet* and only accepts +words with the same type as *alphabet*. + +.. note:: + The constructor fixes whether this object is alphabet-aware (i.e. constructed + from an :any:`Alphabet` object). It also fixes the word type of an + alphabet-aware object. In particular, ``RevLenLexCmp()`` only accepts + ``init()`` (which does nothing); it cannot be changed into an alphabet-aware + object by calling ``init(alphabet)``. Similarly, + ``RevLenLexCmp(alphabet).init(new_alphabet)`` requires *new_alphabet* to have + the same word type as *alphabet*. + +.. seealso:: + + :any:`Alphabet` + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RevLenLexCmp + >>> RevLenLexCmp()("a", "b") + True + >>> RevLenLexCmp()([0], [1]) + True + >>> RevLenLexCmp(Alphabet("ba"))("b", "a") + True +)pbdoc"); + + thing.def(py::init const&>(), + py::arg("alphabet"), + R"pbdoc( +:sig=(self: RevLenLexCmp, alphabet: Alphabet) -> None: +Construct a reversed len-lex comparison object from an alphabet. + +Constructs an object whose call operator first compares words by length and +then compares words of equal length from right to left by the positions of +their letters in *alphabet*. The type of letters in *alphabet* also fixes the +accepted word type for the call operator. + +:param alphabet: the optional alphabet defining the order of letters. +:type alphabet: Alphabet + +:raises TypeError: + if more than one argument is given or the argument is not an :any:`Alphabet`. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RevLenLexCmp + >>> RevLenLexCmp(Alphabet("ba"))("b", "a") + True +)pbdoc"); + + thing.def("__repr__", [](RevLenLexCmp_ const& self) { + return to_human_readable_repr(self); + }); + + thing.def("__copy__", + [](RevLenLexCmp_ const& self) { return RevLenLexCmp_(self); }); + + thing.def( + "copy", + [](RevLenLexCmp_ const& self) { return RevLenLexCmp_(self); }, + R"pbdoc( +:sig=(self: RevLenLexCmp) -> RevLenLexCmp: +Copy a comparison object. + +The copy has the same mode and word type as *self*. For an alphabet-aware +object, the stored alphabet is also copied, so subsequently reinitializing one +comparison object does not affect the other. + +:returns: A copy of *self*. +:rtype: RevLenLexCmp + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RevLenLexCmp + >>> RevLenLexCmp(Alphabet("ba")).copy()("b", "a") + True +)pbdoc"); + + thing.def( + "init", + [](RevLenLexCmp_& self, Alphabet const& alphabet) + -> RevLenLexCmp_& { return self.init(alphabet); }, + py::arg("alphabet"), + R"pbdoc( +:sig=(self: RevLenLexCmp, alphabet: Alphabet) -> RevLenLexCmp: +Reinitialize the comparison object. + +If *self* was constructed using an :any:`Alphabet`, then +``init(new_alphabet)`` puts *self* back into the same state it would have been +had it been newly constructed from *alphabet*. + +:param alphabet: the replacement alphabet. +:type alphabet: Alphabet + +:returns: The first argument *self*. +:rtype: RevLenLexCmp + +:raises TypeError: + if the type of the words in the new alphabet is not the same as the existing + alphabet. +:raises AttributeError: + if *self* was constructed without an alphabet. + +.. warning:: + This method only works if *self* was constructed as + ``RevLenLexCmp(alphabet)``. An object constructed as ``RevLenLexCmp()`` does not have a + stored alphabet. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RevLenLexCmp + >>> compare = RevLenLexCmp(Alphabet("ab")) + >>> compare.init(Alphabet("ba")) is compare + True + >>> compare("b", "a") + True +)pbdoc"); + + thing.def( + "__call__", + [](RevLenLexCmp_ const& self, Word const& x, Word const& y) { + return self(x, y); + }, + py::arg("x"), + py::arg("y"), + R"pbdoc( +:sig=(self: RevLenLexCmp, x: str | list[int], y: str | list[int]) -> bool: +Compare two words using reversed len-lex ordering. + +Words are first compared by length and then lexicographically after being read +from right to left. If *self* was constructed as ``RevLenLexCmp()``, then *x* +and *y* must either both be strings or both be lists of integers, and letters +are compared using their natural order. If *self* was constructed using an +:any:`Alphabet`, then *x* and *y* must have the same type of words as +*alphabet*, and letters are compared by their positions in the alphabet. + +:param x: the first word. +:type x: str | list[int] +:param y: the second word. +:type y: str | list[int] +:returns: Whether *x* is less than *y*. +:rtype: bool + +:raises TypeError: + if *x* and *y* are not both strings or both lists of integers, or if their + type does not match the alphabet used to construct *self*. +:raises LibsemigroupsError: + if *self* is alphabet-aware and either word contains a letter that does not + belong to its alphabet. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import RevLenLexCmp + >>> RevLenLexCmp()("ba", "ab") + True + >>> RevLenLexCmp()([1, 0], [0, 1]) + True +)pbdoc"); + + thing.def("alphabet", + &RevLenLexCmp_::alphabet, + R"pbdoc( +:sig=(self: RevLenLexCmp) -> Alphabet: +Return the alphabet used to compare letters. + +:returns: The stored alphabet. +:rtype: Alphabet + +:raises AttributeError: + if *self* was constructed without an alphabet. + +.. warning:: + This method only works if *self* was constructed as + ``RevLenLexCmp(alphabet)``. An object constructed as ``RevLenLexCmp()`` does not + have a stored alphabet. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, RevLenLexCmp + >>> alphabet = Alphabet("ba") + >>> RevLenLexCmp(alphabet).alphabet() == alphabet + True +)pbdoc", + py::return_value_policy::reference_internal); + } // bind_rev_lenlex_cmp_with_alphabet + } // namespace void init_order(py::module& m) { py::options options; @@ -2393,6 +2592,7 @@ respectively, in new code. bind_cmp_default>(m, "Lex"); bind_cmp_default>(m, "RevLex"); bind_cmp_default>(m, "LenLex"); + bind_cmp_default>(m, "RevLenLex"); bind_cmp_default>(m, "RPO"); bind_cmp_default>(m, "RevRPO"); @@ -2405,6 +2605,9 @@ respectively, in new code. bind_lenlex_cmp_with_alphabet(m, "LenLexCmpString"); bind_lenlex_cmp_with_alphabet(m, "LenLexCmpWord"); + bind_rev_lenlex_cmp_with_alphabet(m, "RevLenLexCmpString"); + bind_rev_lenlex_cmp_with_alphabet(m, "RevLenLexCmpWord"); + bind_rpo_cmp_with_alphabet(m, "RPOCmpString"); bind_rpo_cmp_with_alphabet(m, "RPOCmpWord"); diff --git a/tests/test_order.py b/tests/test_order.py index b94969d2..e3d18730 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -16,6 +16,7 @@ LexCmp, LibsemigroupsError, Order, + RevLenLexCmp, RevLexCmp, RevRPOCmp, RPOCmp, @@ -132,6 +133,37 @@ def test_lenlex_cmp_object_rejects_bad_constructors(): LenLexCmp(Alphabet("abc"), Alphabet("abc")) +def test_rev_lenlex_cmp_object_without_alphabet(): + """Check the stateless comparison object with both word types.""" + compare = RevLenLexCmp() + assert compare("a", "b") + assert compare([0], [1]) + assert compare("ba", "ab") + + +@pytest.mark.parametrize( + ("alphabet", "x", "y", "missing"), + [(Alphabet("ba"), "b", "a", "c"), (Alphabet([1, 0]), [1], [0], [2])], +) +def test_rev_lenlex_cmp_object_with_alphabet(alphabet, x, y, missing): + """Check that the comparison object stores and uses its alphabet.""" + compare = RevLenLexCmp(alphabet=alphabet) + assert compare(x, y) + assert compare.alphabet() == alphabet + assert compare.init(alphabet) is compare + + with pytest.raises(LibsemigroupsError): + compare(x, missing) + + +def test_rev_lenlex_cmp_object_rejects_bad_constructors(): + """Check that only zero arguments or one Alphabet are accepted.""" + with pytest.raises(TypeError): + RevLenLexCmp("abc") + with pytest.raises(TypeError): + RevLenLexCmp(Alphabet("abc"), Alphabet("abc")) + + def test_rpo_cmp_object_without_alphabet(): """Check the stateless comparison object with both word types.""" compare = RPOCmp() @@ -194,7 +226,9 @@ def test_rev_rpo_cmp_object_rejects_bad_constructors(): RevRPOCmp(Alphabet("abc"), Alphabet("abc")) -@pytest.mark.parametrize("comparison_type", [LexCmp, RevLexCmp, LenLexCmp, RPOCmp, RevRPOCmp]) +@pytest.mark.parametrize( + "comparison_type", [LexCmp, RevLexCmp, LenLexCmp, RevLenLexCmp, RPOCmp, RevRPOCmp] +) @pytest.mark.parametrize( ("alphabet", "replacement", "x", "y"), [ @@ -232,6 +266,7 @@ def test_comparison_object_copy(comparison_type, alphabet, replacement, x, y): (LexCmp, "LexCmp"), (RevLexCmp, "RevLexCmp"), (LenLexCmp, "LenLexCmp"), + (RevLenLexCmp, "RevLenLexCmp"), (RPOCmp, "RPOCmp"), (RevRPOCmp, "RevRPOCmp"), ], From 881f7a7581e2816d0756b0111857f20874a7b12d Mon Sep 17 00:00:00 2001 From: CodexAI Date: Mon, 17 Aug 2026 14:50:05 +0100 Subject: [PATCH 06/27] order: support WrCmp --- docs/source/data-structures/order/index.rst | 1 + docs/source/data-structures/order/wr-cmp.rst | 35 +++ src/libsemigroups_pybind11/__init__.py | 3 +- src/libsemigroups_pybind11/order.py | 82 +++++- src/order.cpp | 279 +++++++++++++++++++ tests/test_order.py | 50 ++++ 6 files changed, 448 insertions(+), 2 deletions(-) create mode 100644 docs/source/data-structures/order/wr-cmp.rst diff --git a/docs/source/data-structures/order/index.rst b/docs/source/data-structures/order/index.rst index 2f6fb19d..87f9a741 100644 --- a/docs/source/data-structures/order/index.rst +++ b/docs/source/data-structures/order/index.rst @@ -30,4 +30,5 @@ strings are described on the following pages: rev-lenlex-cmp rpo-cmp rev-rpo-cmp + wr-cmp functions diff --git a/docs/source/data-structures/order/wr-cmp.rst b/docs/source/data-structures/order/wr-cmp.rst new file mode 100644 index 00000000..082dbbdd --- /dev/null +++ b/docs/source/data-structures/order/wr-cmp.rst @@ -0,0 +1,35 @@ +.. + Copyright (c) 2026 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 WrCmp class +=============== + +.. autoclass:: WrCmp + :doc-only: + +Contents +-------- + +.. autosummary:: + :signatures: short + + ~WrCmp + WrCmp.__call__ + WrCmp.alphabet + WrCmp.copy + WrCmp.init + WrCmp.levels + +Full API +-------- + +.. autoclass:: WrCmp + :class-doc-from: init + :special-members: __call__ + :members: diff --git a/src/libsemigroups_pybind11/__init__.py b/src/libsemigroups_pybind11/__init__.py index 77c6dd4e..e06a9712 100644 --- a/src/libsemigroups_pybind11/__init__.py +++ b/src/libsemigroups_pybind11/__init__.py @@ -53,7 +53,7 @@ from .knuth_bendix import KnuthBendix from .konieczny import Konieczny from .matrix import Matrix, MatrixKind -from .order import LenLexCmp, LexCmp, RevLenLexCmp, RevLexCmp, RevRPOCmp, RPOCmp +from .order import LenLexCmp, LexCmp, RevLenLexCmp, RevLexCmp, RevRPOCmp, RPOCmp, WrCmp from .presentation import InversePresentation, Presentation from .schreier_sims import SchreierSims from .sims import MinimalRepOrc, RepOrc, Sims1, Sims2, SimsRefinerFaithful, SimsRefinerIdeals @@ -176,6 +176,7 @@ "RevLenLexCmp", "RevLexCmp", "RevRPOCmp", + "WrCmp", "Meeter", "NegativeInfinity", "Order", diff --git a/src/libsemigroups_pybind11/order.py b/src/libsemigroups_pybind11/order.py index c8f75c90..5640891d 100644 --- a/src/libsemigroups_pybind11/order.py +++ b/src/libsemigroups_pybind11/order.py @@ -27,6 +27,9 @@ RPOCmpDefault as _RPOCmpDefault, RPOCmpString as _RPOCmpString, RPOCmpWord as _RPOCmpWord, + WrCmpDefault as _WrCmpDefault, + WrCmpString as _WrCmpString, + WrCmpWord as _WrCmpWord, ) from .alphabet import Alphabet as _Alphabet @@ -39,6 +42,51 @@ from .detail.decorators import copydoc as _copydoc +class _ConfiguredCmp(_CxxWrapper): + _configuration_name: str + + def __init__(self: _Self, *args, **kwargs) -> None: + super().__init__(*args, optional_kwargs=("alphabet", self._configuration_name), **kwargs) + if _to_cxx(self) is not None: + return + + if len(args) > 2: + raise TypeError(f"expected at most 2 positional arguments, found {len(args)}") + if args and kwargs: + raise TypeError("expected positional or keyword arguments, but found both") + + if kwargs: + alphabet = kwargs.get("alphabet") + configuration = kwargs.get(self._configuration_name) + has_alphabet = "alphabet" in kwargs + has_configuration = self._configuration_name in kwargs + else: + alphabet = args[0] if len(args) == 2 else None + configuration = args[-1] if args else None + has_alphabet = len(args) == 2 + has_configuration = bool(args) + + if not has_alphabet and not has_configuration: + self.py_template_params = () + self.init_cxx_obj() + elif not has_alphabet and isinstance(configuration, list): + self.py_template_params = () + self.init_cxx_obj(configuration) + elif ( + has_alphabet + and has_configuration + and isinstance(alphabet, _Alphabet) + and isinstance(configuration, list) + ): + self.py_template_params = alphabet.py_template_params + self.init_cxx_obj(alphabet, configuration) + else: + raise TypeError( + f"expected either {self._configuration_name}: list[int], or alphabet: " + f"Alphabet and {self._configuration_name}: list[int]" + ) + + class LexCmp(_CxxWrapper): __doc__ = _LexCmpString.__doc__ @@ -357,6 +405,33 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) +class WrCmp(_ConfiguredCmp): + __doc__ = _WrCmpString.__doc__ + _configuration_name = "levels" + + _py_template_params_to_cxx_type = { + (): _WrCmpDefault, + (str,): _WrCmpString, + (list[int],): _WrCmpWord, + } + _cxx_type_to_py_template_params = dict( + zip( + _py_template_params_to_cxx_type.values(), + _py_template_params_to_cxx_type.keys(), + strict=True, + ) + ) + _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} + + @_copydoc(_WrCmpString.__init__, _WrCmpDefault.__init__) + def __init__(self: _Self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + @_copydoc(_WrCmpString.__call__) + def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: + return super().__call__(x, y) + + _copy_cxx_mem_fns(_LenLexCmpString, LenLexCmp) _register_cxx_wrapped_type(_LenLexCmpDefault, LenLexCmp) _register_cxx_wrapped_type(_LenLexCmpString, LenLexCmp) @@ -387,4 +462,9 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: _register_cxx_wrapped_type(_RevRPOCmpString, RevRPOCmp) _register_cxx_wrapped_type(_RevRPOCmpWord, RevRPOCmp) -__all__ = ["LenLexCmp", "LexCmp", "RPOCmp", "RevLenLexCmp", "RevLexCmp", "RevRPOCmp"] +_copy_cxx_mem_fns(_WrCmpString, WrCmp) +_register_cxx_wrapped_type(_WrCmpDefault, WrCmp) +_register_cxx_wrapped_type(_WrCmpString, WrCmp) +_register_cxx_wrapped_type(_WrCmpWord, WrCmp) + +__all__ = ["LenLexCmp", "LexCmp", "RPOCmp", "RevLenLexCmp", "RevLexCmp", "RevRPOCmp", "WrCmp"] diff --git a/src/order.cpp b/src/order.cpp index aa7cccfb..8472ca12 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -1298,6 +1298,268 @@ Constructs an object whose call operator compares either ``str`` or py::arg("y")); } + template + void bind_configured_cmp_default( + py::module& m, + std::string const& name, + std::string const& configuration, + std::vector const& (Cmp::*get_configuration)() const) { + std::string const binding_name = name + "Default"; + py::class_ thing(m, binding_name.c_str()); + + thing.def(py::init<>(), + fmt::format(R"pbdoc( +:sig=(self: {0}) -> None: +Construct a comparison object with an empty {1} vector. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import {0} + >>> {0}().{1}() + [] +)pbdoc", + name, + configuration) + .c_str()); + thing.def(py::init const&>(), + py::arg(configuration.c_str()), + fmt::format(R"pbdoc( +:sig=(self: {0}, {1}: list[int]) -> None: +Construct a comparison object for index words. + +:param {1}: the {1} of the generators. +:type {1}: list[int] + +.. doctest:: python + + >>> from libsemigroups_pybind11 import {0} + >>> {0}([1, 2]).{1}() + [1, 2] +)pbdoc", + name, + configuration) + .c_str()); + thing.def("__repr__", + [](Cmp const& self) { return to_human_readable_repr(self); }); + thing.def("__copy__", [](Cmp const& self) { return Cmp(self); }); + thing.def("copy", [](Cmp const& self) { return Cmp(self); }); + thing.def("init", [](Cmp& self) -> Cmp& { return self.init(); }); + thing.def( + "init", + [](Cmp& self, std::vector const& values) -> Cmp& { + return self.init(values); + }, + py::arg(configuration.c_str())); + thing.def(configuration.c_str(), [get_configuration](Cmp const& self) { + return (self.*get_configuration)(); + }); + thing.def( + "__call__", + [](Cmp const& self, word_type const& x, word_type const& y) { + return self(x, y); + }, + py::arg("x"), + py::arg("y")); + } + + template + void bind_configured_cmp_with_alphabet( + py::module& m, + char const* binding_name, + std::string const& name, + std::string const& configuration, + std::string const& ordering, + std::vector const& (Cmp::*get_configuration)() const) { + py::class_ thing(m, + binding_name, + fmt::format(R"pbdoc( +Compare words using {2}. + +Use ``{0}({1})`` to compare ``list[int]`` index words, where the entries of +*{1}* correspond to the indices. Use ``{0}(alphabet, {1})`` to compare words +whose letters belong to *alphabet*. The latter form copies both arguments and +only accepts words with the same type as *alphabet*. + +.. note:: + The constructor fixes whether this object is alphabet-aware and fixes the + word type of an alphabet-aware object. Reinitialization cannot change either. + +.. seealso:: + + :any:`Alphabet` + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, {0} + >>> {0}([1, 2])([0], [1]) + True + >>> {0}(Alphabet("ab"), [1, 2])("a", "b") + True + >>> {0}(Alphabet([0, 1]), [1, 2])([0], [1]) + True +)pbdoc", + name, + configuration, + ordering) + .c_str()); + + thing.def(py::init const&, std::vector const&>(), + py::arg("alphabet"), + py::arg(configuration.c_str()), + fmt::format(R"pbdoc( +:sig=(self: {0}, alphabet: Alphabet, {1}: list[int]) -> None: +Construct a comparison object from an alphabet and {1}. + +The two arguments must have the same size. + +:param alphabet: the alphabet defining the letters and their order. +:type alphabet: Alphabet +:param {1}: the {1} of the letters in *alphabet*. +:type {1}: list[int] + +:raises LibsemigroupsError: + if *alphabet* and *{1}* have different sizes. +:raises TypeError: + if the arguments do not have the required types. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, {0} + >>> {0}(Alphabet("ab"), [1, 2])("a", "b") + True +)pbdoc", + name, + configuration) + .c_str()); + thing.def("__repr__", + [](Cmp const& self) { return to_human_readable_repr(self); }); + thing.def("__copy__", [](Cmp const& self) { return Cmp(self); }); + thing.def( + "copy", + [](Cmp const& self) { return Cmp(self); }, + fmt::format(R"pbdoc( +:sig=(self: {0}) -> {0}: +Copy a comparison object. + +:returns: An independent copy of *self*. +:rtype: {0} + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, {0} + >>> {0}(Alphabet("ab"), [1, 2]).copy()("a", "b") + True +)pbdoc", + name) + .c_str()); + thing.def( + "init", + [](Cmp& self, + Alphabet const& alphabet, + std::vector const& values) -> Cmp& { + return self.init(alphabet, values); + }, + py::arg("alphabet"), + py::arg(configuration.c_str()), + fmt::format(R"pbdoc( +:sig=(self: {0}, alphabet: Alphabet, {1}: list[int]) -> {0}: +Reinitialize the comparison object. + +For an alphabet-aware object, pass an alphabet of the original word type and a +same-sized {1} vector. For an index-word object, call ``init({1})`` or +``init()``; the latter clears the vector. + +:returns: The first argument *self*. +:rtype: {0} + +:raises AttributeError: + if the arguments do not match the construction mode of *self*. +:raises LibsemigroupsError: + if *alphabet* and *{1}* have different sizes. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, {0} + >>> compare = {0}(Alphabet("ab"), [1, 2]) + >>> compare.init(Alphabet("ba"), [1, 2]) is compare + True +)pbdoc", + name, + configuration) + .c_str()); + thing.def( + "__call__", + [](Cmp const& self, Word const& x, Word const& y) { + return self(x, y); + }, + py::arg("x"), + py::arg("y"), + fmt::format(R"pbdoc( +:sig=(self: {0}, x: str | list[int], y: str | list[int]) -> bool: +Compare two words using {2}. + +Index-word objects accept ``list[int]``. Alphabet-aware objects accept words +of the same type as their alphabet. + +:returns: Whether *x* is less than *y*. +:rtype: bool + +:raises TypeError: + if the word types do not match the construction mode. +:raises LibsemigroupsError: + if a word contains a letter not represented by the stored configuration. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, {0} + >>> {0}([1, 2])([0], [1]) + True + >>> {0}(Alphabet("ab"), [1, 2])("a", "b") + True +)pbdoc", + name, + configuration, + ordering) + .c_str()); + thing.def("alphabet", + &Cmp::alphabet, + fmt::format(R"pbdoc( +:sig=(self: {0}) -> Alphabet: +Return the stored alphabet. + +:raises AttributeError: + if *self* was constructed without an alphabet. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, {0} + >>> alphabet = Alphabet("ab") + >>> {0}(alphabet, [1, 2]).alphabet() == alphabet + True +)pbdoc", + name) + .c_str(), + py::return_value_policy::reference_internal); + thing.def( + configuration.c_str(), + [get_configuration](Cmp const& self) { + return (self.*get_configuration)(); + }, + fmt::format(R"pbdoc( +:sig=(self: {0}) -> list[int]: +Return the stored {1}. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import {0} + >>> {0}([1, 2]).{1}() + [1, 2] +)pbdoc", + name, + configuration) + .c_str()); + } + template void bind_lex_cmp_with_alphabet(py::module& m, char const* name) { using LexCmp_ = LexCmp; @@ -2613,5 +2875,22 @@ respectively, in new code. bind_rev_rpo_cmp_with_alphabet(m, "RevRPOCmpString"); bind_rev_rpo_cmp_with_alphabet(m, "RevRPOCmpWord"); + + bind_configured_cmp_default>( + m, "WrCmp", "levels", &WrCmp<>::levels); + bind_configured_cmp_with_alphabet, std::string>( + m, + "WrCmpString", + "WrCmp", + "levels", + "wreath-product ordering", + &WrCmp::levels); + bind_configured_cmp_with_alphabet, word_type>( + m, + "WrCmpWord", + "WrCmp", + "levels", + "wreath-product ordering", + &WrCmp::levels); } } // namespace libsemigroups diff --git a/tests/test_order.py b/tests/test_order.py index e3d18730..94088051 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -20,6 +20,7 @@ RevLexCmp, RevRPOCmp, RPOCmp, + WrCmp, len_wt_lex_cmp, lenlex_cmp, lex_cmp, @@ -226,6 +227,55 @@ def test_rev_rpo_cmp_object_rejects_bad_constructors(): RevRPOCmp(Alphabet("abc"), Alphabet("abc")) +def test_wr_cmp_object_without_alphabet(): + """Check wreath-product comparison of index words.""" + compare = WrCmp([1, 2]) + assert compare.levels() == [1, 2] + assert compare([0], [1]) + assert compare.init([2, 1]) is compare + assert compare.levels() == [2, 1] + + +@pytest.mark.parametrize( + ("alphabet", "x", "y", "missing"), + [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], +) +def test_wr_cmp_object_with_alphabet(alphabet, x, y, missing): + """Check alphabet-aware wreath-product comparison.""" + compare = WrCmp(alphabet, [1, 2]) + assert compare(x, y) + assert compare.alphabet() == alphabet + assert compare.levels() == [1, 2] + assert compare.init(alphabet, [1, 2]) is compare + + with pytest.raises(LibsemigroupsError): + compare(x, missing) + + +def test_wr_cmp_object_copy_and_repr(): + """Check copies and human-readable representations.""" + original = WrCmp(Alphabet("ab"), [1, 2]) + copies = ( + original.copy(), + original.__copy__(), # pylint: disable=unnecessary-dunder-call + copy(original), + ) + original.init(Alphabet("ab"), [2, 1]) + assert all(copied.levels() == [1, 2] for copied in copies) + assert repr(WrCmp([1, 2])) == "" + assert repr(WrCmp(Alphabet("ab"), [1, 2])) == ( + ' with levels [1, 2]>' + ) + + +def test_wr_cmp_object_rejects_bad_constructors(): + """Check constructor validation.""" + with pytest.raises(TypeError): + WrCmp(Alphabet("ab")) + with pytest.raises(LibsemigroupsError): + WrCmp(Alphabet("ab"), [1]) + + @pytest.mark.parametrize( "comparison_type", [LexCmp, RevLexCmp, LenLexCmp, RevLenLexCmp, RPOCmp, RevRPOCmp] ) From 3279885e800fa87ea491cd768e9134dd2ddcd838 Mon Sep 17 00:00:00 2001 From: CodexAI Date: Mon, 17 Aug 2026 14:51:54 +0100 Subject: [PATCH 07/27] order: support RevWrCmp --- docs/source/data-structures/order/index.rst | 1 + .../data-structures/order/rev-wr-cmp.rst | 35 +++++++++++++ src/libsemigroups_pybind11/__init__.py | 3 +- src/libsemigroups_pybind11/order.py | 48 +++++++++++++++++- src/order.cpp | 17 +++++++ tests/test_order.py | 50 +++++++++++++++++++ 6 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 docs/source/data-structures/order/rev-wr-cmp.rst diff --git a/docs/source/data-structures/order/index.rst b/docs/source/data-structures/order/index.rst index 87f9a741..c24fd1db 100644 --- a/docs/source/data-structures/order/index.rst +++ b/docs/source/data-structures/order/index.rst @@ -31,4 +31,5 @@ strings are described on the following pages: rpo-cmp rev-rpo-cmp wr-cmp + rev-wr-cmp functions diff --git a/docs/source/data-structures/order/rev-wr-cmp.rst b/docs/source/data-structures/order/rev-wr-cmp.rst new file mode 100644 index 00000000..1883a68a --- /dev/null +++ b/docs/source/data-structures/order/rev-wr-cmp.rst @@ -0,0 +1,35 @@ +.. + Copyright (c) 2026 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 RevWrCmp class +================== + +.. autoclass:: RevWrCmp + :doc-only: + +Contents +-------- + +.. autosummary:: + :signatures: short + + ~RevWrCmp + RevWrCmp.__call__ + RevWrCmp.alphabet + RevWrCmp.copy + RevWrCmp.init + RevWrCmp.levels + +Full API +-------- + +.. autoclass:: RevWrCmp + :class-doc-from: init + :special-members: __call__ + :members: diff --git a/src/libsemigroups_pybind11/__init__.py b/src/libsemigroups_pybind11/__init__.py index e06a9712..dea4b50c 100644 --- a/src/libsemigroups_pybind11/__init__.py +++ b/src/libsemigroups_pybind11/__init__.py @@ -53,7 +53,7 @@ from .knuth_bendix import KnuthBendix from .konieczny import Konieczny from .matrix import Matrix, MatrixKind -from .order import LenLexCmp, LexCmp, RevLenLexCmp, RevLexCmp, RevRPOCmp, RPOCmp, WrCmp +from .order import LenLexCmp, LexCmp, RevLenLexCmp, RevLexCmp, RevRPOCmp, RevWrCmp, RPOCmp, WrCmp from .presentation import InversePresentation, Presentation from .schreier_sims import SchreierSims from .sims import MinimalRepOrc, RepOrc, Sims1, Sims2, SimsRefinerFaithful, SimsRefinerIdeals @@ -176,6 +176,7 @@ "RevLenLexCmp", "RevLexCmp", "RevRPOCmp", + "RevWrCmp", "WrCmp", "Meeter", "NegativeInfinity", diff --git a/src/libsemigroups_pybind11/order.py b/src/libsemigroups_pybind11/order.py index 5640891d..17c6d134 100644 --- a/src/libsemigroups_pybind11/order.py +++ b/src/libsemigroups_pybind11/order.py @@ -24,6 +24,9 @@ RevRPOCmpDefault as _RevRPOCmpDefault, RevRPOCmpString as _RevRPOCmpString, RevRPOCmpWord as _RevRPOCmpWord, + RevWrCmpDefault as _RevWrCmpDefault, + RevWrCmpString as _RevWrCmpString, + RevWrCmpWord as _RevWrCmpWord, RPOCmpDefault as _RPOCmpDefault, RPOCmpString as _RPOCmpString, RPOCmpWord as _RPOCmpWord, @@ -467,4 +470,47 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: _register_cxx_wrapped_type(_WrCmpString, WrCmp) _register_cxx_wrapped_type(_WrCmpWord, WrCmp) -__all__ = ["LenLexCmp", "LexCmp", "RPOCmp", "RevLenLexCmp", "RevLexCmp", "RevRPOCmp", "WrCmp"] + +class RevWrCmp(_ConfiguredCmp): + __doc__ = _RevWrCmpString.__doc__ + _configuration_name = "levels" + + _py_template_params_to_cxx_type = { + (): _RevWrCmpDefault, + (str,): _RevWrCmpString, + (list[int],): _RevWrCmpWord, + } + _cxx_type_to_py_template_params = dict( + zip( + _py_template_params_to_cxx_type.values(), + _py_template_params_to_cxx_type.keys(), + strict=True, + ) + ) + _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} + + @_copydoc(_RevWrCmpString.__init__, _RevWrCmpDefault.__init__) + def __init__(self: _Self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + @_copydoc(_RevWrCmpString.__call__) + def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: + return super().__call__(x, y) + + +_copy_cxx_mem_fns(_RevWrCmpString, RevWrCmp) +_register_cxx_wrapped_type(_RevWrCmpDefault, RevWrCmp) +_register_cxx_wrapped_type(_RevWrCmpString, RevWrCmp) +_register_cxx_wrapped_type(_RevWrCmpWord, RevWrCmp) + + +__all__ = [ + "LenLexCmp", + "LexCmp", + "RPOCmp", + "RevLenLexCmp", + "RevLexCmp", + "RevRPOCmp", + "RevWrCmp", + "WrCmp", +] diff --git a/src/order.cpp b/src/order.cpp index 8472ca12..407bade0 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -2892,5 +2892,22 @@ respectively, in new code. "levels", "wreath-product ordering", &WrCmp::levels); + + bind_configured_cmp_default>( + m, "RevWrCmp", "levels", &RevWrCmp<>::levels); + bind_configured_cmp_with_alphabet, std::string>( + m, + "RevWrCmpString", + "RevWrCmp", + "levels", + "reversed wreath-product ordering", + &RevWrCmp::levels); + bind_configured_cmp_with_alphabet, word_type>( + m, + "RevWrCmpWord", + "RevWrCmp", + "levels", + "reversed wreath-product ordering", + &RevWrCmp::levels); } } // namespace libsemigroups diff --git a/tests/test_order.py b/tests/test_order.py index 94088051..d7cb46a0 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -19,6 +19,7 @@ RevLenLexCmp, RevLexCmp, RevRPOCmp, + RevWrCmp, RPOCmp, WrCmp, len_wt_lex_cmp, @@ -49,6 +50,55 @@ def test_lex_cmp_object_without_alphabet(): assert not compare("b", "aa") +def test_rev_wr_cmp_object_without_alphabet(): + """Check reversed wreath-product comparison of index words.""" + compare = RevWrCmp([1, 2]) + assert compare.levels() == [1, 2] + assert compare([0], [1]) + assert compare.init([2, 1]) is compare + assert compare.levels() == [2, 1] + + +@pytest.mark.parametrize( + ("alphabet", "x", "y", "missing"), + [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], +) +def test_rev_wr_cmp_object_with_alphabet(alphabet, x, y, missing): + """Check alphabet-aware reversed wreath-product comparison.""" + compare = RevWrCmp(alphabet, [1, 2]) + assert compare(x, y) + assert compare.alphabet() == alphabet + assert compare.levels() == [1, 2] + assert compare.init(alphabet, [1, 2]) is compare + + with pytest.raises(LibsemigroupsError): + compare(x, missing) + + +def test_rev_wr_cmp_object_copy_and_repr(): + """Check copies and human-readable representations.""" + original = RevWrCmp(Alphabet("ab"), [1, 2]) + copies = ( + original.copy(), + original.__copy__(), # pylint: disable=unnecessary-dunder-call + copy(original), + ) + original.init(Alphabet("ab"), [2, 1]) + assert all(copied.levels() == [1, 2] for copied in copies) + assert repr(RevWrCmp([1, 2])) == "" + assert repr(RevWrCmp(Alphabet("ab"), [1, 2])) == ( + ' with levels [1, 2]>' + ) + + +def test_rev_wr_cmp_object_rejects_bad_constructors(): + """Check constructor validation.""" + with pytest.raises(TypeError): + RevWrCmp(Alphabet("ab")) + with pytest.raises(LibsemigroupsError): + RevWrCmp(Alphabet("ab"), [1]) + + @pytest.mark.parametrize( ("alphabet", "x", "y", "missing"), [(Alphabet("ba"), "b", "a", "c"), (Alphabet([1, 0]), [1], [0], [2])], From 584149d3ddbc682245b648952e44d308d1788ee2 Mon Sep 17 00:00:00 2001 From: CodexAI Date: Mon, 17 Aug 2026 14:54:20 +0100 Subject: [PATCH 08/27] order: support WtLenLexCmp --- docs/source/data-structures/order/index.rst | 1 + .../data-structures/order/wt-lenlex-cmp.rst | 35 ++++++++ src/libsemigroups_pybind11/__init__.py | 13 ++- src/libsemigroups_pybind11/order.py | 90 +++++++++++++------ src/order.cpp | 17 ++++ tests/test_order.py | 50 +++++++++++ 6 files changed, 178 insertions(+), 28 deletions(-) create mode 100644 docs/source/data-structures/order/wt-lenlex-cmp.rst diff --git a/docs/source/data-structures/order/index.rst b/docs/source/data-structures/order/index.rst index c24fd1db..2422893c 100644 --- a/docs/source/data-structures/order/index.rst +++ b/docs/source/data-structures/order/index.rst @@ -32,4 +32,5 @@ strings are described on the following pages: rev-rpo-cmp wr-cmp rev-wr-cmp + wt-lenlex-cmp functions diff --git a/docs/source/data-structures/order/wt-lenlex-cmp.rst b/docs/source/data-structures/order/wt-lenlex-cmp.rst new file mode 100644 index 00000000..e747c9d1 --- /dev/null +++ b/docs/source/data-structures/order/wt-lenlex-cmp.rst @@ -0,0 +1,35 @@ +.. + Copyright (c) 2026 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 WtLenLexCmp class +===================== + +.. autoclass:: WtLenLexCmp + :doc-only: + +Contents +-------- + +.. autosummary:: + :signatures: short + + ~WtLenLexCmp + WtLenLexCmp.__call__ + WtLenLexCmp.alphabet + WtLenLexCmp.copy + WtLenLexCmp.init + WtLenLexCmp.weights + +Full API +-------- + +.. autoclass:: WtLenLexCmp + :class-doc-from: init + :special-members: __call__ + :members: diff --git a/src/libsemigroups_pybind11/__init__.py b/src/libsemigroups_pybind11/__init__.py index dea4b50c..f7f336e2 100644 --- a/src/libsemigroups_pybind11/__init__.py +++ b/src/libsemigroups_pybind11/__init__.py @@ -53,7 +53,17 @@ from .knuth_bendix import KnuthBendix from .konieczny import Konieczny from .matrix import Matrix, MatrixKind -from .order import LenLexCmp, LexCmp, RevLenLexCmp, RevLexCmp, RevRPOCmp, RevWrCmp, RPOCmp, WrCmp +from .order import ( + LenLexCmp, + LexCmp, + RevLenLexCmp, + RevLexCmp, + RevRPOCmp, + RevWrCmp, + RPOCmp, + WrCmp, + WtLenLexCmp, +) from .presentation import InversePresentation, Presentation from .schreier_sims import SchreierSims from .sims import MinimalRepOrc, RepOrc, Sims1, Sims2, SimsRefinerFaithful, SimsRefinerIdeals @@ -178,6 +188,7 @@ "RevRPOCmp", "RevWrCmp", "WrCmp", + "WtLenLexCmp", "Meeter", "NegativeInfinity", "Order", diff --git a/src/libsemigroups_pybind11/order.py b/src/libsemigroups_pybind11/order.py index 17c6d134..6ef8d256 100644 --- a/src/libsemigroups_pybind11/order.py +++ b/src/libsemigroups_pybind11/order.py @@ -33,6 +33,9 @@ WrCmpDefault as _WrCmpDefault, WrCmpString as _WrCmpString, WrCmpWord as _WrCmpWord, + WtLenLexCmpDefault as _WtLenLexCmpDefault, + WtLenLexCmpString as _WtLenLexCmpString, + WtLenLexCmpWord as _WtLenLexCmpWord, ) from .alphabet import Alphabet as _Alphabet @@ -435,6 +438,60 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) +class RevWrCmp(_ConfiguredCmp): + __doc__ = _RevWrCmpString.__doc__ + _configuration_name = "levels" + + _py_template_params_to_cxx_type = { + (): _RevWrCmpDefault, + (str,): _RevWrCmpString, + (list[int],): _RevWrCmpWord, + } + _cxx_type_to_py_template_params = dict( + zip( + _py_template_params_to_cxx_type.values(), + _py_template_params_to_cxx_type.keys(), + strict=True, + ) + ) + _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} + + @_copydoc(_RevWrCmpString.__init__, _RevWrCmpDefault.__init__) + def __init__(self: _Self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + @_copydoc(_RevWrCmpString.__call__) + def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: + return super().__call__(x, y) + + +class WtLenLexCmp(_ConfiguredCmp): + __doc__ = _WtLenLexCmpString.__doc__ + _configuration_name = "weights" + + _py_template_params_to_cxx_type = { + (): _WtLenLexCmpDefault, + (str,): _WtLenLexCmpString, + (list[int],): _WtLenLexCmpWord, + } + _cxx_type_to_py_template_params = dict( + zip( + _py_template_params_to_cxx_type.values(), + _py_template_params_to_cxx_type.keys(), + strict=True, + ) + ) + _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} + + @_copydoc(_WtLenLexCmpString.__init__, _WtLenLexCmpDefault.__init__) + def __init__(self: _Self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + @_copydoc(_WtLenLexCmpString.__call__) + def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: + return super().__call__(x, y) + + _copy_cxx_mem_fns(_LenLexCmpString, LenLexCmp) _register_cxx_wrapped_type(_LenLexCmpDefault, LenLexCmp) _register_cxx_wrapped_type(_LenLexCmpString, LenLexCmp) @@ -471,38 +528,16 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: _register_cxx_wrapped_type(_WrCmpWord, WrCmp) -class RevWrCmp(_ConfiguredCmp): - __doc__ = _RevWrCmpString.__doc__ - _configuration_name = "levels" - - _py_template_params_to_cxx_type = { - (): _RevWrCmpDefault, - (str,): _RevWrCmpString, - (list[int],): _RevWrCmpWord, - } - _cxx_type_to_py_template_params = dict( - zip( - _py_template_params_to_cxx_type.values(), - _py_template_params_to_cxx_type.keys(), - strict=True, - ) - ) - _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} - - @_copydoc(_RevWrCmpString.__init__, _RevWrCmpDefault.__init__) - def __init__(self: _Self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - - @_copydoc(_RevWrCmpString.__call__) - def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: - return super().__call__(x, y) - - _copy_cxx_mem_fns(_RevWrCmpString, RevWrCmp) _register_cxx_wrapped_type(_RevWrCmpDefault, RevWrCmp) _register_cxx_wrapped_type(_RevWrCmpString, RevWrCmp) _register_cxx_wrapped_type(_RevWrCmpWord, RevWrCmp) +_copy_cxx_mem_fns(_WtLenLexCmpString, WtLenLexCmp) +_register_cxx_wrapped_type(_WtLenLexCmpDefault, WtLenLexCmp) +_register_cxx_wrapped_type(_WtLenLexCmpString, WtLenLexCmp) +_register_cxx_wrapped_type(_WtLenLexCmpWord, WtLenLexCmp) + __all__ = [ "LenLexCmp", @@ -513,4 +548,5 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: "RevRPOCmp", "RevWrCmp", "WrCmp", + "WtLenLexCmp", ] diff --git a/src/order.cpp b/src/order.cpp index 407bade0..fb5e96fa 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -2909,5 +2909,22 @@ respectively, in new code. "levels", "reversed wreath-product ordering", &RevWrCmp::levels); + + bind_configured_cmp_default>( + m, "WtLenLexCmp", "weights", &WtLenLexCmp<>::weights); + bind_configured_cmp_with_alphabet, std::string>( + m, + "WtLenLexCmpString", + "WtLenLexCmp", + "weights", + "weighted len-lex ordering", + &WtLenLexCmp::weights); + bind_configured_cmp_with_alphabet, word_type>( + m, + "WtLenLexCmpWord", + "WtLenLexCmp", + "weights", + "weighted len-lex ordering", + &WtLenLexCmp::weights); } } // namespace libsemigroups diff --git a/tests/test_order.py b/tests/test_order.py index d7cb46a0..d9b75569 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -22,6 +22,7 @@ RevWrCmp, RPOCmp, WrCmp, + WtLenLexCmp, len_wt_lex_cmp, lenlex_cmp, lex_cmp, @@ -326,6 +327,55 @@ def test_wr_cmp_object_rejects_bad_constructors(): WrCmp(Alphabet("ab"), [1]) +def test_wt_lenlex_cmp_object_without_alphabet(): + """Check weighted len-lex comparison of index words.""" + compare = WtLenLexCmp([1, 2]) + assert compare.weights() == [1, 2] + assert compare([0], [1]) + assert compare.init([2, 1]) is compare + assert compare.weights() == [2, 1] + + +@pytest.mark.parametrize( + ("alphabet", "x", "y", "missing"), + [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], +) +def test_wt_lenlex_cmp_object_with_alphabet(alphabet, x, y, missing): + """Check alphabet-aware weighted len-lex comparison.""" + compare = WtLenLexCmp(alphabet, [1, 2]) + assert compare(x, y) + assert compare.alphabet() == alphabet + assert compare.weights() == [1, 2] + assert compare.init(alphabet, [1, 2]) is compare + + with pytest.raises(LibsemigroupsError): + compare(x, missing) + + +def test_wt_lenlex_cmp_object_copy_and_repr(): + """Check copies and human-readable representations.""" + original = WtLenLexCmp(Alphabet("ab"), [1, 2]) + copies = ( + original.copy(), + original.__copy__(), # pylint: disable=unnecessary-dunder-call + copy(original), + ) + original.init(Alphabet("ab"), [2, 1]) + assert all(copied.weights() == [1, 2] for copied in copies) + assert repr(WtLenLexCmp([1, 2])) == "" + assert repr(WtLenLexCmp(Alphabet("ab"), [1, 2])) == ( + ' with weights [1, 2]>' + ) + + +def test_wt_lenlex_cmp_object_rejects_bad_constructors(): + """Check constructor validation.""" + with pytest.raises(TypeError): + WtLenLexCmp(Alphabet("ab")) + with pytest.raises(LibsemigroupsError): + WtLenLexCmp(Alphabet("ab"), [1]) + + @pytest.mark.parametrize( "comparison_type", [LexCmp, RevLexCmp, LenLexCmp, RevLenLexCmp, RPOCmp, RevRPOCmp] ) From 09d7136666e1db60e30e60d6b62effd05ac553f0 Mon Sep 17 00:00:00 2001 From: CodexAI Date: Mon, 17 Aug 2026 14:55:49 +0100 Subject: [PATCH 09/27] order: support RevWtLenLexCmp --- docs/source/data-structures/order/index.rst | 1 + .../order/rev-wt-lenlex-cmp.rst | 35 +++++++++++++ src/libsemigroups_pybind11/__init__.py | 2 + src/libsemigroups_pybind11/order.py | 37 ++++++++++++++ src/order.cpp | 17 +++++++ tests/test_order.py | 50 +++++++++++++++++++ 6 files changed, 142 insertions(+) create mode 100644 docs/source/data-structures/order/rev-wt-lenlex-cmp.rst diff --git a/docs/source/data-structures/order/index.rst b/docs/source/data-structures/order/index.rst index 2422893c..0696f2cc 100644 --- a/docs/source/data-structures/order/index.rst +++ b/docs/source/data-structures/order/index.rst @@ -33,4 +33,5 @@ strings are described on the following pages: wr-cmp rev-wr-cmp wt-lenlex-cmp + rev-wt-lenlex-cmp functions diff --git a/docs/source/data-structures/order/rev-wt-lenlex-cmp.rst b/docs/source/data-structures/order/rev-wt-lenlex-cmp.rst new file mode 100644 index 00000000..3f08065c --- /dev/null +++ b/docs/source/data-structures/order/rev-wt-lenlex-cmp.rst @@ -0,0 +1,35 @@ +.. + Copyright (c) 2026 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 RevWtLenLexCmp class +======================== + +.. autoclass:: RevWtLenLexCmp + :doc-only: + +Contents +-------- + +.. autosummary:: + :signatures: short + + ~RevWtLenLexCmp + RevWtLenLexCmp.__call__ + RevWtLenLexCmp.alphabet + RevWtLenLexCmp.copy + RevWtLenLexCmp.init + RevWtLenLexCmp.weights + +Full API +-------- + +.. autoclass:: RevWtLenLexCmp + :class-doc-from: init + :special-members: __call__ + :members: diff --git a/src/libsemigroups_pybind11/__init__.py b/src/libsemigroups_pybind11/__init__.py index f7f336e2..4fa8d569 100644 --- a/src/libsemigroups_pybind11/__init__.py +++ b/src/libsemigroups_pybind11/__init__.py @@ -60,6 +60,7 @@ RevLexCmp, RevRPOCmp, RevWrCmp, + RevWtLenLexCmp, RPOCmp, WrCmp, WtLenLexCmp, @@ -187,6 +188,7 @@ "RevLexCmp", "RevRPOCmp", "RevWrCmp", + "RevWtLenLexCmp", "WrCmp", "WtLenLexCmp", "Meeter", diff --git a/src/libsemigroups_pybind11/order.py b/src/libsemigroups_pybind11/order.py index 6ef8d256..e58733f6 100644 --- a/src/libsemigroups_pybind11/order.py +++ b/src/libsemigroups_pybind11/order.py @@ -27,6 +27,9 @@ RevWrCmpDefault as _RevWrCmpDefault, RevWrCmpString as _RevWrCmpString, RevWrCmpWord as _RevWrCmpWord, + RevWtLenLexCmpDefault as _RevWtLenLexCmpDefault, + RevWtLenLexCmpString as _RevWtLenLexCmpString, + RevWtLenLexCmpWord as _RevWtLenLexCmpWord, RPOCmpDefault as _RPOCmpDefault, RPOCmpString as _RPOCmpString, RPOCmpWord as _RPOCmpWord, @@ -539,6 +542,39 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: _register_cxx_wrapped_type(_WtLenLexCmpWord, WtLenLexCmp) +class RevWtLenLexCmp(_ConfiguredCmp): + __doc__ = _RevWtLenLexCmpString.__doc__ + _configuration_name = "weights" + + _py_template_params_to_cxx_type = { + (): _RevWtLenLexCmpDefault, + (str,): _RevWtLenLexCmpString, + (list[int],): _RevWtLenLexCmpWord, + } + _cxx_type_to_py_template_params = dict( + zip( + _py_template_params_to_cxx_type.values(), + _py_template_params_to_cxx_type.keys(), + strict=True, + ) + ) + _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} + + @_copydoc(_RevWtLenLexCmpString.__init__, _RevWtLenLexCmpDefault.__init__) + def __init__(self: _Self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + @_copydoc(_RevWtLenLexCmpString.__call__) + def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: + return super().__call__(x, y) + + +_copy_cxx_mem_fns(_RevWtLenLexCmpString, RevWtLenLexCmp) +_register_cxx_wrapped_type(_RevWtLenLexCmpDefault, RevWtLenLexCmp) +_register_cxx_wrapped_type(_RevWtLenLexCmpString, RevWtLenLexCmp) +_register_cxx_wrapped_type(_RevWtLenLexCmpWord, RevWtLenLexCmp) + + __all__ = [ "LenLexCmp", "LexCmp", @@ -547,6 +583,7 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: "RevLexCmp", "RevRPOCmp", "RevWrCmp", + "RevWtLenLexCmp", "WrCmp", "WtLenLexCmp", ] diff --git a/src/order.cpp b/src/order.cpp index fb5e96fa..dcf004a7 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -2926,5 +2926,22 @@ respectively, in new code. "weights", "weighted len-lex ordering", &WtLenLexCmp::weights); + + bind_configured_cmp_default>( + m, "RevWtLenLexCmp", "weights", &RevWtLenLexCmp<>::weights); + bind_configured_cmp_with_alphabet, std::string>( + m, + "RevWtLenLexCmpString", + "RevWtLenLexCmp", + "weights", + "reversed weighted len-lex ordering", + &RevWtLenLexCmp::weights); + bind_configured_cmp_with_alphabet, word_type>( + m, + "RevWtLenLexCmpWord", + "RevWtLenLexCmp", + "weights", + "reversed weighted len-lex ordering", + &RevWtLenLexCmp::weights); } } // namespace libsemigroups diff --git a/tests/test_order.py b/tests/test_order.py index d9b75569..11607f97 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -20,6 +20,7 @@ RevLexCmp, RevRPOCmp, RevWrCmp, + RevWtLenLexCmp, RPOCmp, WrCmp, WtLenLexCmp, @@ -376,6 +377,55 @@ def test_wt_lenlex_cmp_object_rejects_bad_constructors(): WtLenLexCmp(Alphabet("ab"), [1]) +def test_rev_wt_lenlex_cmp_object_without_alphabet(): + """Check reversed weighted len-lex comparison of index words.""" + compare = RevWtLenLexCmp([1, 2]) + assert compare.weights() == [1, 2] + assert compare([0], [1]) + assert compare.init([2, 1]) is compare + assert compare.weights() == [2, 1] + + +@pytest.mark.parametrize( + ("alphabet", "x", "y", "missing"), + [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], +) +def test_rev_wt_lenlex_cmp_object_with_alphabet(alphabet, x, y, missing): + """Check alphabet-aware reversed weighted len-lex comparison.""" + compare = RevWtLenLexCmp(alphabet, [1, 2]) + assert compare(x, y) + assert compare.alphabet() == alphabet + assert compare.weights() == [1, 2] + assert compare.init(alphabet, [1, 2]) is compare + + with pytest.raises(LibsemigroupsError): + compare(x, missing) + + +def test_rev_wt_lenlex_cmp_object_copy_and_repr(): + """Check copies and human-readable representations.""" + original = RevWtLenLexCmp(Alphabet("ab"), [1, 2]) + copies = ( + original.copy(), + original.__copy__(), # pylint: disable=unnecessary-dunder-call + copy(original), + ) + original.init(Alphabet("ab"), [2, 1]) + assert all(copied.weights() == [1, 2] for copied in copies) + assert repr(RevWtLenLexCmp([1, 2])) == "" + assert repr(RevWtLenLexCmp(Alphabet("ab"), [1, 2])) == ( + ' with weights [1, 2]>' + ) + + +def test_rev_wt_lenlex_cmp_object_rejects_bad_constructors(): + """Check constructor validation.""" + with pytest.raises(TypeError): + RevWtLenLexCmp(Alphabet("ab")) + with pytest.raises(LibsemigroupsError): + RevWtLenLexCmp(Alphabet("ab"), [1]) + + @pytest.mark.parametrize( "comparison_type", [LexCmp, RevLexCmp, LenLexCmp, RevLenLexCmp, RPOCmp, RevRPOCmp] ) From 01762eb3038d65067cbe3124270533ea4255252f Mon Sep 17 00:00:00 2001 From: CodexAI Date: Mon, 17 Aug 2026 14:57:59 +0100 Subject: [PATCH 10/27] order: support WtLexCmp --- docs/source/data-structures/order/index.rst | 1 + .../data-structures/order/wt-lex-cmp.rst | 35 +++++++ src/libsemigroups_pybind11/__init__.py | 2 + src/libsemigroups_pybind11/order.py | 91 +++++++++++++------ src/order.cpp | 17 ++++ tests/test_order.py | 50 ++++++++++ 6 files changed, 169 insertions(+), 27 deletions(-) create mode 100644 docs/source/data-structures/order/wt-lex-cmp.rst diff --git a/docs/source/data-structures/order/index.rst b/docs/source/data-structures/order/index.rst index 0696f2cc..c9123c84 100644 --- a/docs/source/data-structures/order/index.rst +++ b/docs/source/data-structures/order/index.rst @@ -34,4 +34,5 @@ strings are described on the following pages: rev-wr-cmp wt-lenlex-cmp rev-wt-lenlex-cmp + wt-lex-cmp functions diff --git a/docs/source/data-structures/order/wt-lex-cmp.rst b/docs/source/data-structures/order/wt-lex-cmp.rst new file mode 100644 index 00000000..50358009 --- /dev/null +++ b/docs/source/data-structures/order/wt-lex-cmp.rst @@ -0,0 +1,35 @@ +.. + Copyright (c) 2026 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 WtLexCmp class +================== + +.. autoclass:: WtLexCmp + :doc-only: + +Contents +-------- + +.. autosummary:: + :signatures: short + + ~WtLexCmp + WtLexCmp.__call__ + WtLexCmp.alphabet + WtLexCmp.copy + WtLexCmp.init + WtLexCmp.weights + +Full API +-------- + +.. autoclass:: WtLexCmp + :class-doc-from: init + :special-members: __call__ + :members: diff --git a/src/libsemigroups_pybind11/__init__.py b/src/libsemigroups_pybind11/__init__.py index 4fa8d569..bc057a0b 100644 --- a/src/libsemigroups_pybind11/__init__.py +++ b/src/libsemigroups_pybind11/__init__.py @@ -64,6 +64,7 @@ RPOCmp, WrCmp, WtLenLexCmp, + WtLexCmp, ) from .presentation import InversePresentation, Presentation from .schreier_sims import SchreierSims @@ -191,6 +192,7 @@ "RevWtLenLexCmp", "WrCmp", "WtLenLexCmp", + "WtLexCmp", "Meeter", "NegativeInfinity", "Order", diff --git a/src/libsemigroups_pybind11/order.py b/src/libsemigroups_pybind11/order.py index e58733f6..f91e6001 100644 --- a/src/libsemigroups_pybind11/order.py +++ b/src/libsemigroups_pybind11/order.py @@ -39,6 +39,9 @@ WtLenLexCmpDefault as _WtLenLexCmpDefault, WtLenLexCmpString as _WtLenLexCmpString, WtLenLexCmpWord as _WtLenLexCmpWord, + WtLexCmpDefault as _WtLexCmpDefault, + WtLexCmpString as _WtLexCmpString, + WtLexCmpWord as _WtLexCmpWord, ) from .alphabet import Alphabet as _Alphabet @@ -495,6 +498,60 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) +class RevWtLenLexCmp(_ConfiguredCmp): + __doc__ = _RevWtLenLexCmpString.__doc__ + _configuration_name = "weights" + + _py_template_params_to_cxx_type = { + (): _RevWtLenLexCmpDefault, + (str,): _RevWtLenLexCmpString, + (list[int],): _RevWtLenLexCmpWord, + } + _cxx_type_to_py_template_params = dict( + zip( + _py_template_params_to_cxx_type.values(), + _py_template_params_to_cxx_type.keys(), + strict=True, + ) + ) + _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} + + @_copydoc(_RevWtLenLexCmpString.__init__, _RevWtLenLexCmpDefault.__init__) + def __init__(self: _Self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + @_copydoc(_RevWtLenLexCmpString.__call__) + def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: + return super().__call__(x, y) + + +class WtLexCmp(_ConfiguredCmp): + __doc__ = _WtLexCmpString.__doc__ + _configuration_name = "weights" + + _py_template_params_to_cxx_type = { + (): _WtLexCmpDefault, + (str,): _WtLexCmpString, + (list[int],): _WtLexCmpWord, + } + _cxx_type_to_py_template_params = dict( + zip( + _py_template_params_to_cxx_type.values(), + _py_template_params_to_cxx_type.keys(), + strict=True, + ) + ) + _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} + + @_copydoc(_WtLexCmpString.__init__, _WtLexCmpDefault.__init__) + def __init__(self: _Self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + @_copydoc(_WtLexCmpString.__call__) + def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: + return super().__call__(x, y) + + _copy_cxx_mem_fns(_LenLexCmpString, LenLexCmp) _register_cxx_wrapped_type(_LenLexCmpDefault, LenLexCmp) _register_cxx_wrapped_type(_LenLexCmpString, LenLexCmp) @@ -542,39 +599,18 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: _register_cxx_wrapped_type(_WtLenLexCmpWord, WtLenLexCmp) -class RevWtLenLexCmp(_ConfiguredCmp): - __doc__ = _RevWtLenLexCmpString.__doc__ - _configuration_name = "weights" - - _py_template_params_to_cxx_type = { - (): _RevWtLenLexCmpDefault, - (str,): _RevWtLenLexCmpString, - (list[int],): _RevWtLenLexCmpWord, - } - _cxx_type_to_py_template_params = dict( - zip( - _py_template_params_to_cxx_type.values(), - _py_template_params_to_cxx_type.keys(), - strict=True, - ) - ) - _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} - - @_copydoc(_RevWtLenLexCmpString.__init__, _RevWtLenLexCmpDefault.__init__) - def __init__(self: _Self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - - @_copydoc(_RevWtLenLexCmpString.__call__) - def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: - return super().__call__(x, y) - - _copy_cxx_mem_fns(_RevWtLenLexCmpString, RevWtLenLexCmp) _register_cxx_wrapped_type(_RevWtLenLexCmpDefault, RevWtLenLexCmp) _register_cxx_wrapped_type(_RevWtLenLexCmpString, RevWtLenLexCmp) _register_cxx_wrapped_type(_RevWtLenLexCmpWord, RevWtLenLexCmp) +_copy_cxx_mem_fns(_WtLexCmpString, WtLexCmp) +_register_cxx_wrapped_type(_WtLexCmpDefault, WtLexCmp) +_register_cxx_wrapped_type(_WtLexCmpString, WtLexCmp) +_register_cxx_wrapped_type(_WtLexCmpWord, WtLexCmp) + + __all__ = [ "LenLexCmp", "LexCmp", @@ -586,4 +622,5 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: "RevWtLenLexCmp", "WrCmp", "WtLenLexCmp", + "WtLexCmp", ] diff --git a/src/order.cpp b/src/order.cpp index dcf004a7..751d5cad 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -2943,5 +2943,22 @@ respectively, in new code. "weights", "reversed weighted len-lex ordering", &RevWtLenLexCmp::weights); + + bind_configured_cmp_default>( + m, "WtLexCmp", "weights", &WtLexCmp<>::weights); + bind_configured_cmp_with_alphabet, std::string>( + m, + "WtLexCmpString", + "WtLexCmp", + "weights", + "weighted lexicographic ordering", + &WtLexCmp::weights); + bind_configured_cmp_with_alphabet, word_type>( + m, + "WtLexCmpWord", + "WtLexCmp", + "weights", + "weighted lexicographic ordering", + &WtLexCmp::weights); } } // namespace libsemigroups diff --git a/tests/test_order.py b/tests/test_order.py index 11607f97..f3985fa6 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -24,6 +24,7 @@ RPOCmp, WrCmp, WtLenLexCmp, + WtLexCmp, len_wt_lex_cmp, lenlex_cmp, lex_cmp, @@ -426,6 +427,55 @@ def test_rev_wt_lenlex_cmp_object_rejects_bad_constructors(): RevWtLenLexCmp(Alphabet("ab"), [1]) +def test_wt_lex_cmp_object_without_alphabet(): + """Check weighted lexicographic comparison of index words.""" + compare = WtLexCmp([1, 2]) + assert compare.weights() == [1, 2] + assert compare([0], [1]) + assert compare.init([2, 1]) is compare + assert compare.weights() == [2, 1] + + +@pytest.mark.parametrize( + ("alphabet", "x", "y", "missing"), + [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], +) +def test_wt_lex_cmp_object_with_alphabet(alphabet, x, y, missing): + """Check alphabet-aware weighted lexicographic comparison.""" + compare = WtLexCmp(alphabet, [1, 2]) + assert compare(x, y) + assert compare.alphabet() == alphabet + assert compare.weights() == [1, 2] + assert compare.init(alphabet, [1, 2]) is compare + + with pytest.raises(LibsemigroupsError): + compare(x, missing) + + +def test_wt_lex_cmp_object_copy_and_repr(): + """Check copies and human-readable representations.""" + original = WtLexCmp(Alphabet("ab"), [1, 2]) + copies = ( + original.copy(), + original.__copy__(), # pylint: disable=unnecessary-dunder-call + copy(original), + ) + original.init(Alphabet("ab"), [2, 1]) + assert all(copied.weights() == [1, 2] for copied in copies) + assert repr(WtLexCmp([1, 2])) == "" + assert repr(WtLexCmp(Alphabet("ab"), [1, 2])) == ( + ' with weights [1, 2]>' + ) + + +def test_wt_lex_cmp_object_rejects_bad_constructors(): + """Check constructor validation.""" + with pytest.raises(TypeError): + WtLexCmp(Alphabet("ab")) + with pytest.raises(LibsemigroupsError): + WtLexCmp(Alphabet("ab"), [1]) + + @pytest.mark.parametrize( "comparison_type", [LexCmp, RevLexCmp, LenLexCmp, RevLenLexCmp, RPOCmp, RevRPOCmp] ) From 9207b3799017c9c309daea5c75c2203ce0a41738 Mon Sep 17 00:00:00 2001 From: CodexAI Date: Mon, 17 Aug 2026 14:59:07 +0100 Subject: [PATCH 11/27] order: support RevWtLexCmp --- docs/source/data-structures/order/index.rst | 1 + .../data-structures/order/rev-wt-lex-cmp.rst | 35 +++++++++++++ src/libsemigroups_pybind11/__init__.py | 2 + src/libsemigroups_pybind11/order.py | 37 ++++++++++++++ src/order.cpp | 17 +++++++ tests/test_order.py | 50 +++++++++++++++++++ 6 files changed, 142 insertions(+) create mode 100644 docs/source/data-structures/order/rev-wt-lex-cmp.rst diff --git a/docs/source/data-structures/order/index.rst b/docs/source/data-structures/order/index.rst index c9123c84..74266605 100644 --- a/docs/source/data-structures/order/index.rst +++ b/docs/source/data-structures/order/index.rst @@ -35,4 +35,5 @@ strings are described on the following pages: wt-lenlex-cmp rev-wt-lenlex-cmp wt-lex-cmp + rev-wt-lex-cmp functions diff --git a/docs/source/data-structures/order/rev-wt-lex-cmp.rst b/docs/source/data-structures/order/rev-wt-lex-cmp.rst new file mode 100644 index 00000000..bf1be420 --- /dev/null +++ b/docs/source/data-structures/order/rev-wt-lex-cmp.rst @@ -0,0 +1,35 @@ +.. + Copyright (c) 2026 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 RevWtLexCmp class +===================== + +.. autoclass:: RevWtLexCmp + :doc-only: + +Contents +-------- + +.. autosummary:: + :signatures: short + + ~RevWtLexCmp + RevWtLexCmp.__call__ + RevWtLexCmp.alphabet + RevWtLexCmp.copy + RevWtLexCmp.init + RevWtLexCmp.weights + +Full API +-------- + +.. autoclass:: RevWtLexCmp + :class-doc-from: init + :special-members: __call__ + :members: diff --git a/src/libsemigroups_pybind11/__init__.py b/src/libsemigroups_pybind11/__init__.py index bc057a0b..a53a95e6 100644 --- a/src/libsemigroups_pybind11/__init__.py +++ b/src/libsemigroups_pybind11/__init__.py @@ -61,6 +61,7 @@ RevRPOCmp, RevWrCmp, RevWtLenLexCmp, + RevWtLexCmp, RPOCmp, WrCmp, WtLenLexCmp, @@ -190,6 +191,7 @@ "RevRPOCmp", "RevWrCmp", "RevWtLenLexCmp", + "RevWtLexCmp", "WrCmp", "WtLenLexCmp", "WtLexCmp", diff --git a/src/libsemigroups_pybind11/order.py b/src/libsemigroups_pybind11/order.py index f91e6001..f75b82a6 100644 --- a/src/libsemigroups_pybind11/order.py +++ b/src/libsemigroups_pybind11/order.py @@ -30,6 +30,9 @@ RevWtLenLexCmpDefault as _RevWtLenLexCmpDefault, RevWtLenLexCmpString as _RevWtLenLexCmpString, RevWtLenLexCmpWord as _RevWtLenLexCmpWord, + RevWtLexCmpDefault as _RevWtLexCmpDefault, + RevWtLexCmpString as _RevWtLexCmpString, + RevWtLexCmpWord as _RevWtLexCmpWord, RPOCmpDefault as _RPOCmpDefault, RPOCmpString as _RPOCmpString, RPOCmpWord as _RPOCmpWord, @@ -611,6 +614,39 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: _register_cxx_wrapped_type(_WtLexCmpWord, WtLexCmp) +class RevWtLexCmp(_ConfiguredCmp): + __doc__ = _RevWtLexCmpString.__doc__ + _configuration_name = "weights" + + _py_template_params_to_cxx_type = { + (): _RevWtLexCmpDefault, + (str,): _RevWtLexCmpString, + (list[int],): _RevWtLexCmpWord, + } + _cxx_type_to_py_template_params = dict( + zip( + _py_template_params_to_cxx_type.values(), + _py_template_params_to_cxx_type.keys(), + strict=True, + ) + ) + _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} + + @_copydoc(_RevWtLexCmpString.__init__, _RevWtLexCmpDefault.__init__) + def __init__(self: _Self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + @_copydoc(_RevWtLexCmpString.__call__) + def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: + return super().__call__(x, y) + + +_copy_cxx_mem_fns(_RevWtLexCmpString, RevWtLexCmp) +_register_cxx_wrapped_type(_RevWtLexCmpDefault, RevWtLexCmp) +_register_cxx_wrapped_type(_RevWtLexCmpString, RevWtLexCmp) +_register_cxx_wrapped_type(_RevWtLexCmpWord, RevWtLexCmp) + + __all__ = [ "LenLexCmp", "LexCmp", @@ -620,6 +656,7 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: "RevRPOCmp", "RevWrCmp", "RevWtLenLexCmp", + "RevWtLexCmp", "WrCmp", "WtLenLexCmp", "WtLexCmp", diff --git a/src/order.cpp b/src/order.cpp index 751d5cad..f2f15322 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -2960,5 +2960,22 @@ respectively, in new code. "weights", "weighted lexicographic ordering", &WtLexCmp::weights); + + bind_configured_cmp_default>( + m, "RevWtLexCmp", "weights", &RevWtLexCmp<>::weights); + bind_configured_cmp_with_alphabet, std::string>( + m, + "RevWtLexCmpString", + "RevWtLexCmp", + "weights", + "reversed weighted lexicographic ordering", + &RevWtLexCmp::weights); + bind_configured_cmp_with_alphabet, word_type>( + m, + "RevWtLexCmpWord", + "RevWtLexCmp", + "weights", + "reversed weighted lexicographic ordering", + &RevWtLexCmp::weights); } } // namespace libsemigroups diff --git a/tests/test_order.py b/tests/test_order.py index f3985fa6..995f21b6 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -21,6 +21,7 @@ RevRPOCmp, RevWrCmp, RevWtLenLexCmp, + RevWtLexCmp, RPOCmp, WrCmp, WtLenLexCmp, @@ -476,6 +477,55 @@ def test_wt_lex_cmp_object_rejects_bad_constructors(): WtLexCmp(Alphabet("ab"), [1]) +def test_rev_wt_lex_cmp_object_without_alphabet(): + """Check reversed weighted lexicographic comparison of index words.""" + compare = RevWtLexCmp([1, 2]) + assert compare.weights() == [1, 2] + assert compare([0], [1]) + assert compare.init([2, 1]) is compare + assert compare.weights() == [2, 1] + + +@pytest.mark.parametrize( + ("alphabet", "x", "y", "missing"), + [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], +) +def test_rev_wt_lex_cmp_object_with_alphabet(alphabet, x, y, missing): + """Check alphabet-aware reversed weighted lexicographic comparison.""" + compare = RevWtLexCmp(alphabet, [1, 2]) + assert compare(x, y) + assert compare.alphabet() == alphabet + assert compare.weights() == [1, 2] + assert compare.init(alphabet, [1, 2]) is compare + + with pytest.raises(LibsemigroupsError): + compare(x, missing) + + +def test_rev_wt_lex_cmp_object_copy_and_repr(): + """Check copies and human-readable representations.""" + original = RevWtLexCmp(Alphabet("ab"), [1, 2]) + copies = ( + original.copy(), + original.__copy__(), # pylint: disable=unnecessary-dunder-call + copy(original), + ) + original.init(Alphabet("ab"), [2, 1]) + assert all(copied.weights() == [1, 2] for copied in copies) + assert repr(RevWtLexCmp([1, 2])) == "" + assert repr(RevWtLexCmp(Alphabet("ab"), [1, 2])) == ( + ' with weights [1, 2]>' + ) + + +def test_rev_wt_lex_cmp_object_rejects_bad_constructors(): + """Check constructor validation.""" + with pytest.raises(TypeError): + RevWtLexCmp(Alphabet("ab")) + with pytest.raises(LibsemigroupsError): + RevWtLexCmp(Alphabet("ab"), [1]) + + @pytest.mark.parametrize( "comparison_type", [LexCmp, RevLexCmp, LenLexCmp, RevLenLexCmp, RPOCmp, RevRPOCmp] ) From fe10bac8c344e43183303c844d8bfac251641dc0 Mon Sep 17 00:00:00 2001 From: CodexAI Date: Mon, 17 Aug 2026 15:00:18 +0100 Subject: [PATCH 12/27] order: support LenWtLexCmp --- docs/source/data-structures/order/index.rst | 1 + .../data-structures/order/len-wt-lex-cmp.rst | 35 +++++++++++++ src/libsemigroups_pybind11/__init__.py | 2 + src/libsemigroups_pybind11/order.py | 37 ++++++++++++++ src/order.cpp | 17 +++++++ tests/test_order.py | 50 +++++++++++++++++++ 6 files changed, 142 insertions(+) create mode 100644 docs/source/data-structures/order/len-wt-lex-cmp.rst diff --git a/docs/source/data-structures/order/index.rst b/docs/source/data-structures/order/index.rst index 74266605..30c19959 100644 --- a/docs/source/data-structures/order/index.rst +++ b/docs/source/data-structures/order/index.rst @@ -36,4 +36,5 @@ strings are described on the following pages: rev-wt-lenlex-cmp wt-lex-cmp rev-wt-lex-cmp + len-wt-lex-cmp functions diff --git a/docs/source/data-structures/order/len-wt-lex-cmp.rst b/docs/source/data-structures/order/len-wt-lex-cmp.rst new file mode 100644 index 00000000..5e158899 --- /dev/null +++ b/docs/source/data-structures/order/len-wt-lex-cmp.rst @@ -0,0 +1,35 @@ +.. + Copyright (c) 2026 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 LenWtLexCmp class +===================== + +.. autoclass:: LenWtLexCmp + :doc-only: + +Contents +-------- + +.. autosummary:: + :signatures: short + + ~LenWtLexCmp + LenWtLexCmp.__call__ + LenWtLexCmp.alphabet + LenWtLexCmp.copy + LenWtLexCmp.init + LenWtLexCmp.weights + +Full API +-------- + +.. autoclass:: LenWtLexCmp + :class-doc-from: init + :special-members: __call__ + :members: diff --git a/src/libsemigroups_pybind11/__init__.py b/src/libsemigroups_pybind11/__init__.py index a53a95e6..196eafcb 100644 --- a/src/libsemigroups_pybind11/__init__.py +++ b/src/libsemigroups_pybind11/__init__.py @@ -55,6 +55,7 @@ from .matrix import Matrix, MatrixKind from .order import ( LenLexCmp, + LenWtLexCmp, LexCmp, RevLenLexCmp, RevLexCmp, @@ -184,6 +185,7 @@ "LibsemigroupsError", "LimitMax", "LenLexCmp", + "LenWtLexCmp", "LexCmp", "RPOCmp", "RevLenLexCmp", diff --git a/src/libsemigroups_pybind11/order.py b/src/libsemigroups_pybind11/order.py index f75b82a6..c7828d6a 100644 --- a/src/libsemigroups_pybind11/order.py +++ b/src/libsemigroups_pybind11/order.py @@ -12,6 +12,9 @@ LenLexCmpDefault as _LenLexCmpDefault, LenLexCmpString as _LenLexCmpString, LenLexCmpWord as _LenLexCmpWord, + LenWtLexCmpDefault as _LenWtLexCmpDefault, + LenWtLexCmpString as _LenWtLexCmpString, + LenWtLexCmpWord as _LenWtLexCmpWord, LexCmpDefault as _LexCmpDefault, LexCmpString as _LexCmpString, LexCmpWord as _LexCmpWord, @@ -647,8 +650,42 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: _register_cxx_wrapped_type(_RevWtLexCmpWord, RevWtLexCmp) +class LenWtLexCmp(_ConfiguredCmp): + __doc__ = _LenWtLexCmpString.__doc__ + _configuration_name = "weights" + + _py_template_params_to_cxx_type = { + (): _LenWtLexCmpDefault, + (str,): _LenWtLexCmpString, + (list[int],): _LenWtLexCmpWord, + } + _cxx_type_to_py_template_params = dict( + zip( + _py_template_params_to_cxx_type.values(), + _py_template_params_to_cxx_type.keys(), + strict=True, + ) + ) + _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} + + @_copydoc(_LenWtLexCmpString.__init__, _LenWtLexCmpDefault.__init__) + def __init__(self: _Self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + @_copydoc(_LenWtLexCmpString.__call__) + def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: + return super().__call__(x, y) + + +_copy_cxx_mem_fns(_LenWtLexCmpString, LenWtLexCmp) +_register_cxx_wrapped_type(_LenWtLexCmpDefault, LenWtLexCmp) +_register_cxx_wrapped_type(_LenWtLexCmpString, LenWtLexCmp) +_register_cxx_wrapped_type(_LenWtLexCmpWord, LenWtLexCmp) + + __all__ = [ "LenLexCmp", + "LenWtLexCmp", "LexCmp", "RPOCmp", "RevLenLexCmp", diff --git a/src/order.cpp b/src/order.cpp index f2f15322..496d0b17 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -2977,5 +2977,22 @@ respectively, in new code. "weights", "reversed weighted lexicographic ordering", &RevWtLexCmp::weights); + + bind_configured_cmp_default>( + m, "LenWtLexCmp", "weights", &LenWtLexCmp<>::weights); + bind_configured_cmp_with_alphabet, std::string>( + m, + "LenWtLexCmpString", + "LenWtLexCmp", + "weights", + "length then weighted lexicographic ordering", + &LenWtLexCmp::weights); + bind_configured_cmp_with_alphabet, word_type>( + m, + "LenWtLexCmpWord", + "LenWtLexCmp", + "weights", + "length then weighted lexicographic ordering", + &LenWtLexCmp::weights); } } // namespace libsemigroups diff --git a/tests/test_order.py b/tests/test_order.py index 995f21b6..dc9f9d0a 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -13,6 +13,7 @@ from libsemigroups_pybind11 import ( Alphabet, LenLexCmp, + LenWtLexCmp, LexCmp, LibsemigroupsError, Order, @@ -477,6 +478,55 @@ def test_wt_lex_cmp_object_rejects_bad_constructors(): WtLexCmp(Alphabet("ab"), [1]) +def test_len_wt_lex_cmp_object_without_alphabet(): + """Check length then weighted lexicographic comparison of index words.""" + compare = LenWtLexCmp([1, 2]) + assert compare.weights() == [1, 2] + assert compare([0], [1]) + assert compare.init([2, 1]) is compare + assert compare.weights() == [2, 1] + + +@pytest.mark.parametrize( + ("alphabet", "x", "y", "missing"), + [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], +) +def test_len_wt_lex_cmp_object_with_alphabet(alphabet, x, y, missing): + """Check alphabet-aware length then weighted lexicographic comparison.""" + compare = LenWtLexCmp(alphabet, [1, 2]) + assert compare(x, y) + assert compare.alphabet() == alphabet + assert compare.weights() == [1, 2] + assert compare.init(alphabet, [1, 2]) is compare + + with pytest.raises(LibsemigroupsError): + compare(x, missing) + + +def test_len_wt_lex_cmp_object_copy_and_repr(): + """Check copies and human-readable representations.""" + original = LenWtLexCmp(Alphabet("ab"), [1, 2]) + copies = ( + original.copy(), + original.__copy__(), # pylint: disable=unnecessary-dunder-call + copy(original), + ) + original.init(Alphabet("ab"), [2, 1]) + assert all(copied.weights() == [1, 2] for copied in copies) + assert repr(LenWtLexCmp([1, 2])) == "" + assert repr(LenWtLexCmp(Alphabet("ab"), [1, 2])) == ( + ' with weights [1, 2]>' + ) + + +def test_len_wt_lex_cmp_object_rejects_bad_constructors(): + """Check constructor validation.""" + with pytest.raises(TypeError): + LenWtLexCmp(Alphabet("ab")) + with pytest.raises(LibsemigroupsError): + LenWtLexCmp(Alphabet("ab"), [1]) + + def test_rev_wt_lex_cmp_object_without_alphabet(): """Check reversed weighted lexicographic comparison of index words.""" compare = RevWtLexCmp([1, 2]) From 050e8dcc7578da93ac00c9de5095685784f05e22 Mon Sep 17 00:00:00 2001 From: CodexAI Date: Mon, 17 Aug 2026 15:01:37 +0100 Subject: [PATCH 13/27] order: support RevLenWtLexCmp --- docs/source/data-structures/order/index.rst | 1 + .../order/rev-len-wt-lex-cmp.rst | 35 +++++++++++++ src/libsemigroups_pybind11/__init__.py | 2 + src/libsemigroups_pybind11/order.py | 37 ++++++++++++++ src/order.cpp | 17 +++++++ tests/test_order.py | 50 +++++++++++++++++++ 6 files changed, 142 insertions(+) create mode 100644 docs/source/data-structures/order/rev-len-wt-lex-cmp.rst diff --git a/docs/source/data-structures/order/index.rst b/docs/source/data-structures/order/index.rst index 30c19959..b3196b17 100644 --- a/docs/source/data-structures/order/index.rst +++ b/docs/source/data-structures/order/index.rst @@ -37,4 +37,5 @@ strings are described on the following pages: wt-lex-cmp rev-wt-lex-cmp len-wt-lex-cmp + rev-len-wt-lex-cmp functions diff --git a/docs/source/data-structures/order/rev-len-wt-lex-cmp.rst b/docs/source/data-structures/order/rev-len-wt-lex-cmp.rst new file mode 100644 index 00000000..d372c0f2 --- /dev/null +++ b/docs/source/data-structures/order/rev-len-wt-lex-cmp.rst @@ -0,0 +1,35 @@ +.. + Copyright (c) 2026 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 RevLenWtLexCmp class +======================== + +.. autoclass:: RevLenWtLexCmp + :doc-only: + +Contents +-------- + +.. autosummary:: + :signatures: short + + ~RevLenWtLexCmp + RevLenWtLexCmp.__call__ + RevLenWtLexCmp.alphabet + RevLenWtLexCmp.copy + RevLenWtLexCmp.init + RevLenWtLexCmp.weights + +Full API +-------- + +.. autoclass:: RevLenWtLexCmp + :class-doc-from: init + :special-members: __call__ + :members: diff --git a/src/libsemigroups_pybind11/__init__.py b/src/libsemigroups_pybind11/__init__.py index 196eafcb..5aaccce4 100644 --- a/src/libsemigroups_pybind11/__init__.py +++ b/src/libsemigroups_pybind11/__init__.py @@ -58,6 +58,7 @@ LenWtLexCmp, LexCmp, RevLenLexCmp, + RevLenWtLexCmp, RevLexCmp, RevRPOCmp, RevWrCmp, @@ -189,6 +190,7 @@ "LexCmp", "RPOCmp", "RevLenLexCmp", + "RevLenWtLexCmp", "RevLexCmp", "RevRPOCmp", "RevWrCmp", diff --git a/src/libsemigroups_pybind11/order.py b/src/libsemigroups_pybind11/order.py index c7828d6a..15772208 100644 --- a/src/libsemigroups_pybind11/order.py +++ b/src/libsemigroups_pybind11/order.py @@ -21,6 +21,9 @@ RevLenLexCmpDefault as _RevLenLexCmpDefault, RevLenLexCmpString as _RevLenLexCmpString, RevLenLexCmpWord as _RevLenLexCmpWord, + RevLenWtLexCmpDefault as _RevLenWtLexCmpDefault, + RevLenWtLexCmpString as _RevLenWtLexCmpString, + RevLenWtLexCmpWord as _RevLenWtLexCmpWord, RevLexCmpDefault as _RevLexCmpDefault, RevLexCmpString as _RevLexCmpString, RevLexCmpWord as _RevLexCmpWord, @@ -683,12 +686,46 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: _register_cxx_wrapped_type(_LenWtLexCmpWord, LenWtLexCmp) +class RevLenWtLexCmp(_ConfiguredCmp): + __doc__ = _RevLenWtLexCmpString.__doc__ + _configuration_name = "weights" + + _py_template_params_to_cxx_type = { + (): _RevLenWtLexCmpDefault, + (str,): _RevLenWtLexCmpString, + (list[int],): _RevLenWtLexCmpWord, + } + _cxx_type_to_py_template_params = dict( + zip( + _py_template_params_to_cxx_type.values(), + _py_template_params_to_cxx_type.keys(), + strict=True, + ) + ) + _all_wrapped_cxx_types = {*_py_template_params_to_cxx_type.values()} + + @_copydoc(_RevLenWtLexCmpString.__init__, _RevLenWtLexCmpDefault.__init__) + def __init__(self: _Self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + @_copydoc(_RevLenWtLexCmpString.__call__) + def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: + return super().__call__(x, y) + + +_copy_cxx_mem_fns(_RevLenWtLexCmpString, RevLenWtLexCmp) +_register_cxx_wrapped_type(_RevLenWtLexCmpDefault, RevLenWtLexCmp) +_register_cxx_wrapped_type(_RevLenWtLexCmpString, RevLenWtLexCmp) +_register_cxx_wrapped_type(_RevLenWtLexCmpWord, RevLenWtLexCmp) + + __all__ = [ "LenLexCmp", "LenWtLexCmp", "LexCmp", "RPOCmp", "RevLenLexCmp", + "RevLenWtLexCmp", "RevLexCmp", "RevRPOCmp", "RevWrCmp", diff --git a/src/order.cpp b/src/order.cpp index 496d0b17..aefe95e7 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -2994,5 +2994,22 @@ respectively, in new code. "weights", "length then weighted lexicographic ordering", &LenWtLexCmp::weights); + + bind_configured_cmp_default>( + m, "RevLenWtLexCmp", "weights", &RevLenWtLexCmp<>::weights); + bind_configured_cmp_with_alphabet, std::string>( + m, + "RevLenWtLexCmpString", + "RevLenWtLexCmp", + "weights", + "length then reversed weighted lexicographic ordering", + &RevLenWtLexCmp::weights); + bind_configured_cmp_with_alphabet, word_type>( + m, + "RevLenWtLexCmpWord", + "RevLenWtLexCmp", + "weights", + "length then reversed weighted lexicographic ordering", + &RevLenWtLexCmp::weights); } } // namespace libsemigroups diff --git a/tests/test_order.py b/tests/test_order.py index dc9f9d0a..0c79371d 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -18,6 +18,7 @@ LibsemigroupsError, Order, RevLenLexCmp, + RevLenWtLexCmp, RevLexCmp, RevRPOCmp, RevWrCmp, @@ -527,6 +528,55 @@ def test_len_wt_lex_cmp_object_rejects_bad_constructors(): LenWtLexCmp(Alphabet("ab"), [1]) +def test_rev_len_wt_lex_cmp_object_without_alphabet(): + """Check length then reversed weighted lexicographic comparison of index words.""" + compare = RevLenWtLexCmp([1, 2]) + assert compare.weights() == [1, 2] + assert compare([0], [1]) + assert compare.init([2, 1]) is compare + assert compare.weights() == [2, 1] + + +@pytest.mark.parametrize( + ("alphabet", "x", "y", "missing"), + [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], +) +def test_rev_len_wt_lex_cmp_object_with_alphabet(alphabet, x, y, missing): + """Check alphabet-aware length then reversed weighted lexicographic comparison.""" + compare = RevLenWtLexCmp(alphabet, [1, 2]) + assert compare(x, y) + assert compare.alphabet() == alphabet + assert compare.weights() == [1, 2] + assert compare.init(alphabet, [1, 2]) is compare + + with pytest.raises(LibsemigroupsError): + compare(x, missing) + + +def test_rev_len_wt_lex_cmp_object_copy_and_repr(): + """Check copies and human-readable representations.""" + original = RevLenWtLexCmp(Alphabet("ab"), [1, 2]) + copies = ( + original.copy(), + original.__copy__(), # pylint: disable=unnecessary-dunder-call + copy(original), + ) + original.init(Alphabet("ab"), [2, 1]) + assert all(copied.weights() == [1, 2] for copied in copies) + assert repr(RevLenWtLexCmp([1, 2])) == "" + assert repr(RevLenWtLexCmp(Alphabet("ab"), [1, 2])) == ( + ' with weights [1, 2]>' + ) + + +def test_rev_len_wt_lex_cmp_object_rejects_bad_constructors(): + """Check constructor validation.""" + with pytest.raises(TypeError): + RevLenWtLexCmp(Alphabet("ab")) + with pytest.raises(LibsemigroupsError): + RevLenWtLexCmp(Alphabet("ab"), [1]) + + def test_rev_wt_lex_cmp_object_without_alphabet(): """Check reversed weighted lexicographic comparison of index words.""" compare = RevWtLexCmp([1, 2]) From db10e1de679a49c0c216feec6404d184708c6164 Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Wed, 19 Aug 2026 14:29:19 +0100 Subject: [PATCH 14/27] dnr: disable lint warning --- src/libsemigroups_pybind11/du_narendran_rusinowitch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libsemigroups_pybind11/du_narendran_rusinowitch.py b/src/libsemigroups_pybind11/du_narendran_rusinowitch.py index 7fd7e3f5..1916de01 100644 --- a/src/libsemigroups_pybind11/du_narendran_rusinowitch.py +++ b/src/libsemigroups_pybind11/du_narendran_rusinowitch.py @@ -6,6 +6,7 @@ """Subpackage for importing and wrapping :any:`du_narendran_rusinowitch`.""" +# pylint: disable=no-name-in-module from _libsemigroups_pybind11 import du_narendran_rusinowitch as _du_narendran_rusinowitch from .detail.cxx_wrapper import wrap_cxx_free_fn as _wrap_cxx_free_fn From da59f43a266e9ab06ab3807f0dd47de56250f721 Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Wed, 19 Aug 2026 14:39:12 +0100 Subject: [PATCH 15/27] order: reorg file --- src/libsemigroups_pybind11/order.py | 136 +++++++++++++--------------- 1 file changed, 65 insertions(+), 71 deletions(-) diff --git a/src/libsemigroups_pybind11/order.py b/src/libsemigroups_pybind11/order.py index 15772208..db0deca6 100644 --- a/src/libsemigroups_pybind11/order.py +++ b/src/libsemigroups_pybind11/order.py @@ -561,65 +561,6 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) -_copy_cxx_mem_fns(_LenLexCmpString, LenLexCmp) -_register_cxx_wrapped_type(_LenLexCmpDefault, LenLexCmp) -_register_cxx_wrapped_type(_LenLexCmpString, LenLexCmp) -_register_cxx_wrapped_type(_LenLexCmpWord, LenLexCmp) - -_copy_cxx_mem_fns(_RevLenLexCmpString, RevLenLexCmp) -_register_cxx_wrapped_type(_RevLenLexCmpDefault, RevLenLexCmp) -_register_cxx_wrapped_type(_RevLenLexCmpString, RevLenLexCmp) -_register_cxx_wrapped_type(_RevLenLexCmpWord, RevLenLexCmp) - -_copy_cxx_mem_fns(_LexCmpString, LexCmp) -_register_cxx_wrapped_type(_LexCmpDefault, LexCmp) -_register_cxx_wrapped_type(_LexCmpString, LexCmp) -_register_cxx_wrapped_type(_LexCmpWord, LexCmp) - -_copy_cxx_mem_fns(_RevLexCmpString, RevLexCmp) -_register_cxx_wrapped_type(_RevLexCmpDefault, RevLexCmp) -_register_cxx_wrapped_type(_RevLexCmpString, RevLexCmp) -_register_cxx_wrapped_type(_RevLexCmpWord, RevLexCmp) - -_copy_cxx_mem_fns(_RPOCmpString, RPOCmp) -_register_cxx_wrapped_type(_RPOCmpDefault, RPOCmp) -_register_cxx_wrapped_type(_RPOCmpString, RPOCmp) -_register_cxx_wrapped_type(_RPOCmpWord, RPOCmp) - -_copy_cxx_mem_fns(_RevRPOCmpString, RevRPOCmp) -_register_cxx_wrapped_type(_RevRPOCmpDefault, RevRPOCmp) -_register_cxx_wrapped_type(_RevRPOCmpString, RevRPOCmp) -_register_cxx_wrapped_type(_RevRPOCmpWord, RevRPOCmp) - -_copy_cxx_mem_fns(_WrCmpString, WrCmp) -_register_cxx_wrapped_type(_WrCmpDefault, WrCmp) -_register_cxx_wrapped_type(_WrCmpString, WrCmp) -_register_cxx_wrapped_type(_WrCmpWord, WrCmp) - - -_copy_cxx_mem_fns(_RevWrCmpString, RevWrCmp) -_register_cxx_wrapped_type(_RevWrCmpDefault, RevWrCmp) -_register_cxx_wrapped_type(_RevWrCmpString, RevWrCmp) -_register_cxx_wrapped_type(_RevWrCmpWord, RevWrCmp) - -_copy_cxx_mem_fns(_WtLenLexCmpString, WtLenLexCmp) -_register_cxx_wrapped_type(_WtLenLexCmpDefault, WtLenLexCmp) -_register_cxx_wrapped_type(_WtLenLexCmpString, WtLenLexCmp) -_register_cxx_wrapped_type(_WtLenLexCmpWord, WtLenLexCmp) - - -_copy_cxx_mem_fns(_RevWtLenLexCmpString, RevWtLenLexCmp) -_register_cxx_wrapped_type(_RevWtLenLexCmpDefault, RevWtLenLexCmp) -_register_cxx_wrapped_type(_RevWtLenLexCmpString, RevWtLenLexCmp) -_register_cxx_wrapped_type(_RevWtLenLexCmpWord, RevWtLenLexCmp) - - -_copy_cxx_mem_fns(_WtLexCmpString, WtLexCmp) -_register_cxx_wrapped_type(_WtLexCmpDefault, WtLexCmp) -_register_cxx_wrapped_type(_WtLexCmpString, WtLexCmp) -_register_cxx_wrapped_type(_WtLexCmpWord, WtLexCmp) - - class RevWtLexCmp(_ConfiguredCmp): __doc__ = _RevWtLexCmpString.__doc__ _configuration_name = "weights" @@ -647,12 +588,6 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) -_copy_cxx_mem_fns(_RevWtLexCmpString, RevWtLexCmp) -_register_cxx_wrapped_type(_RevWtLexCmpDefault, RevWtLexCmp) -_register_cxx_wrapped_type(_RevWtLexCmpString, RevWtLexCmp) -_register_cxx_wrapped_type(_RevWtLexCmpWord, RevWtLexCmp) - - class LenWtLexCmp(_ConfiguredCmp): __doc__ = _LenWtLexCmpString.__doc__ _configuration_name = "weights" @@ -680,12 +615,6 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) -_copy_cxx_mem_fns(_LenWtLexCmpString, LenWtLexCmp) -_register_cxx_wrapped_type(_LenWtLexCmpDefault, LenWtLexCmp) -_register_cxx_wrapped_type(_LenWtLexCmpString, LenWtLexCmp) -_register_cxx_wrapped_type(_LenWtLexCmpWord, LenWtLexCmp) - - class RevLenWtLexCmp(_ConfiguredCmp): __doc__ = _RevLenWtLexCmpString.__doc__ _configuration_name = "weights" @@ -713,6 +642,71 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) +_copy_cxx_mem_fns(_LenLexCmpString, LenLexCmp) +_register_cxx_wrapped_type(_LenLexCmpDefault, LenLexCmp) +_register_cxx_wrapped_type(_LenLexCmpString, LenLexCmp) +_register_cxx_wrapped_type(_LenLexCmpWord, LenLexCmp) + +_copy_cxx_mem_fns(_RevLenLexCmpString, RevLenLexCmp) +_register_cxx_wrapped_type(_RevLenLexCmpDefault, RevLenLexCmp) +_register_cxx_wrapped_type(_RevLenLexCmpString, RevLenLexCmp) +_register_cxx_wrapped_type(_RevLenLexCmpWord, RevLenLexCmp) + +_copy_cxx_mem_fns(_LexCmpString, LexCmp) +_register_cxx_wrapped_type(_LexCmpDefault, LexCmp) +_register_cxx_wrapped_type(_LexCmpString, LexCmp) +_register_cxx_wrapped_type(_LexCmpWord, LexCmp) + +_copy_cxx_mem_fns(_RevLexCmpString, RevLexCmp) +_register_cxx_wrapped_type(_RevLexCmpDefault, RevLexCmp) +_register_cxx_wrapped_type(_RevLexCmpString, RevLexCmp) +_register_cxx_wrapped_type(_RevLexCmpWord, RevLexCmp) + +_copy_cxx_mem_fns(_RPOCmpString, RPOCmp) +_register_cxx_wrapped_type(_RPOCmpDefault, RPOCmp) +_register_cxx_wrapped_type(_RPOCmpString, RPOCmp) +_register_cxx_wrapped_type(_RPOCmpWord, RPOCmp) + +_copy_cxx_mem_fns(_RevRPOCmpString, RevRPOCmp) +_register_cxx_wrapped_type(_RevRPOCmpDefault, RevRPOCmp) +_register_cxx_wrapped_type(_RevRPOCmpString, RevRPOCmp) +_register_cxx_wrapped_type(_RevRPOCmpWord, RevRPOCmp) + +_copy_cxx_mem_fns(_WrCmpString, WrCmp) +_register_cxx_wrapped_type(_WrCmpDefault, WrCmp) +_register_cxx_wrapped_type(_WrCmpString, WrCmp) +_register_cxx_wrapped_type(_WrCmpWord, WrCmp) + +_copy_cxx_mem_fns(_RevWrCmpString, RevWrCmp) +_register_cxx_wrapped_type(_RevWrCmpDefault, RevWrCmp) +_register_cxx_wrapped_type(_RevWrCmpString, RevWrCmp) +_register_cxx_wrapped_type(_RevWrCmpWord, RevWrCmp) + +_copy_cxx_mem_fns(_WtLenLexCmpString, WtLenLexCmp) +_register_cxx_wrapped_type(_WtLenLexCmpDefault, WtLenLexCmp) +_register_cxx_wrapped_type(_WtLenLexCmpString, WtLenLexCmp) +_register_cxx_wrapped_type(_WtLenLexCmpWord, WtLenLexCmp) + +_copy_cxx_mem_fns(_RevWtLenLexCmpString, RevWtLenLexCmp) +_register_cxx_wrapped_type(_RevWtLenLexCmpDefault, RevWtLenLexCmp) +_register_cxx_wrapped_type(_RevWtLenLexCmpString, RevWtLenLexCmp) +_register_cxx_wrapped_type(_RevWtLenLexCmpWord, RevWtLenLexCmp) + +_copy_cxx_mem_fns(_WtLexCmpString, WtLexCmp) +_register_cxx_wrapped_type(_WtLexCmpDefault, WtLexCmp) +_register_cxx_wrapped_type(_WtLexCmpString, WtLexCmp) +_register_cxx_wrapped_type(_WtLexCmpWord, WtLexCmp) + +_copy_cxx_mem_fns(_RevWtLexCmpString, RevWtLexCmp) +_register_cxx_wrapped_type(_RevWtLexCmpDefault, RevWtLexCmp) +_register_cxx_wrapped_type(_RevWtLexCmpString, RevWtLexCmp) +_register_cxx_wrapped_type(_RevWtLexCmpWord, RevWtLexCmp) + +_copy_cxx_mem_fns(_LenWtLexCmpString, LenWtLexCmp) +_register_cxx_wrapped_type(_LenWtLexCmpDefault, LenWtLexCmp) +_register_cxx_wrapped_type(_LenWtLexCmpString, LenWtLexCmp) +_register_cxx_wrapped_type(_LenWtLexCmpWord, LenWtLexCmp) + _copy_cxx_mem_fns(_RevLenWtLexCmpString, RevLenWtLexCmp) _register_cxx_wrapped_type(_RevLenWtLexCmpDefault, RevLenWtLexCmp) _register_cxx_wrapped_type(_RevLenWtLexCmpString, RevLenWtLexCmp) From f5b6c089fb9e93463b87edb77b187dc0194dc32e Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Wed, 19 Aug 2026 14:53:34 +0100 Subject: [PATCH 16/27] Doc fixes --- src/libsemigroups_pybind11/order.py | 12 +++++ src/order.cpp | 75 ++++++++++++++++++++++++++--- tests/test_order.py | 30 ++++++++++++ 3 files changed, 109 insertions(+), 8 deletions(-) diff --git a/src/libsemigroups_pybind11/order.py b/src/libsemigroups_pybind11/order.py index db0deca6..c5d454fc 100644 --- a/src/libsemigroups_pybind11/order.py +++ b/src/libsemigroups_pybind11/order.py @@ -614,6 +614,12 @@ def __init__(self: _Self, *args, **kwargs) -> None: def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) + @_copydoc(_LenWtLexCmpDefault.init, _LenWtLexCmpString.init) + def init(self: _Self, *args) -> _Self: + cxx_self = _to_cxx(self) + result = cxx_self.init(*(_to_cxx(arg) for arg in args)) + return self if result is cxx_self else result + class RevLenWtLexCmp(_ConfiguredCmp): __doc__ = _RevLenWtLexCmpString.__doc__ @@ -641,6 +647,12 @@ def __init__(self: _Self, *args, **kwargs) -> None: def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) + @_copydoc(_RevLenWtLexCmpDefault.init, _RevLenWtLexCmpString.init) + def init(self: _Self, *args) -> _Self: + cxx_self = _to_cxx(self) + result = cxx_self.init(*(_to_cxx(arg) for arg in args)) + return self if result is cxx_self else result + _copy_cxx_mem_fns(_LenLexCmpString, LenLexCmp) _register_cxx_wrapped_type(_LenLexCmpDefault, LenLexCmp) diff --git a/src/order.cpp b/src/order.cpp index aefe95e7..db59b5f1 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -1343,13 +1343,56 @@ Construct a comparison object for index words. [](Cmp const& self) { return to_human_readable_repr(self); }); thing.def("__copy__", [](Cmp const& self) { return Cmp(self); }); thing.def("copy", [](Cmp const& self) { return Cmp(self); }); - thing.def("init", [](Cmp& self) -> Cmp& { return self.init(); }); + thing.def( + "init", + [](Cmp& self) -> Cmp& { return self.init(); }, + fmt::format(R"pbdoc( +:sig=(self: {0}) -> {0}: +Reinitialize an index-word comparison object with an empty {1} vector. + +:returns: The first argument *self*. +:rtype: {0} + +.. doctest:: python + + >>> from libsemigroups_pybind11 import {0} + >>> compare = {0}([1, 2]) + >>> compare.init() is compare + True + >>> compare.{1}() + [] +)pbdoc", + name, + configuration) + .c_str()); thing.def( "init", [](Cmp& self, std::vector const& values) -> Cmp& { return self.init(values); }, - py::arg(configuration.c_str())); + py::arg(configuration.c_str()), + fmt::format(R"pbdoc( +:sig=(self: {0}, {1}: list[int]) -> {0}: +Reinitialize an index-word comparison object from a {1} vector. + +:param {1}: the {1} of the generators. +:type {1}: list[int] + +:returns: The first argument *self*. +:rtype: {0} + +.. doctest:: python + + >>> from libsemigroups_pybind11 import {0} + >>> compare = {0}() + >>> compare.init([2, 1]) is compare + True + >>> compare.{1}() + [2, 1] +)pbdoc", + name, + configuration) + .c_str()); thing.def(configuration.c_str(), [get_configuration](Cmp const& self) { return (self.*get_configuration)(); }); @@ -1463,19 +1506,23 @@ Copy a comparison object. py::arg(configuration.c_str()), fmt::format(R"pbdoc( :sig=(self: {0}, alphabet: Alphabet, {1}: list[int]) -> {0}: -Reinitialize the comparison object. +Reinitialize an alphabet-aware comparison object. + +The alphabet must have the original word type, and the alphabet and {1} vector +must have the same size. -For an alphabet-aware object, pass an alphabet of the original word type and a -same-sized {1} vector. For an index-word object, call ``init({1})`` or -``init()``; the latter clears the vector. +:param alphabet: the alphabet defining the letters and their order. +:type alphabet: Alphabet +:param {1}: the {1} of the letters in *alphabet*. +:type {1}: list[int] :returns: The first argument *self*. :rtype: {0} -:raises AttributeError: - if the arguments do not match the construction mode of *self*. :raises LibsemigroupsError: if *alphabet* and *{1}* have different sizes. +:raises TypeError: + if the arguments do not have the required types. .. doctest:: python @@ -1487,6 +1534,7 @@ same-sized {1} vector. For an index-word object, call ``init({1})`` or name, configuration) .c_str()); + thing.def( "__call__", [](Cmp const& self, Word const& x, Word const& y) { @@ -1501,6 +1549,11 @@ Compare two words using {2}. Index-word objects accept ``list[int]``. Alphabet-aware objects accept words of the same type as their alphabet. +:param x: the first word. +:type x: str | list[int] +:param y: the second word. +:type y: str | list[int] + :returns: Whether *x* is less than *y*. :rtype: bool @@ -1527,6 +1580,9 @@ of the same type as their alphabet. :sig=(self: {0}) -> Alphabet: Return the stored alphabet. +:returns: The stored alphabet. +:rtype: Alphabet + :raises AttributeError: if *self* was constructed without an alphabet. @@ -1549,6 +1605,9 @@ Return the stored alphabet. :sig=(self: {0}) -> list[int]: Return the stored {1}. +:returns: The stored {1}. +:rtype: list[int] + .. doctest:: python >>> from libsemigroups_pybind11 import {0} diff --git a/tests/test_order.py b/tests/test_order.py index 0c79371d..dc910c40 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -488,6 +488,25 @@ def test_len_wt_lex_cmp_object_without_alphabet(): assert compare.weights() == [2, 1] +def test_len_wt_lex_cmp_init_documentation(): + """Check both C++ init variants are included in the Python documentation.""" + assert "Reinitialize an index-word comparison object with an empty" in ( + LenWtLexCmp.init.__doc__ + ) + assert "Reinitialize an index-word comparison object from a weights" in ( + LenWtLexCmp.init.__doc__ + ) + assert "Reinitialize an alphabet-aware comparison object" in (LenWtLexCmp.init.__doc__) + + +def test_len_wt_lex_cmp_accessor_return_documentation(): + """Check accessor return values and types are documented.""" + assert ":returns: The stored alphabet." in LenWtLexCmp.alphabet.__doc__ + assert ":rtype: Alphabet" in LenWtLexCmp.alphabet.__doc__ + assert ":returns: The stored weights." in LenWtLexCmp.weights.__doc__ + assert ":rtype: list[int]" in LenWtLexCmp.weights.__doc__ + + @pytest.mark.parametrize( ("alphabet", "x", "y", "missing"), [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], @@ -537,6 +556,17 @@ def test_rev_len_wt_lex_cmp_object_without_alphabet(): assert compare.weights() == [2, 1] +def test_rev_len_wt_lex_cmp_init_documentation(): + """Check both C++ init variants are included in the Python documentation.""" + assert "Reinitialize an index-word comparison object with an empty" in ( + RevLenWtLexCmp.init.__doc__ + ) + assert "Reinitialize an index-word comparison object from a weights" in ( + RevLenWtLexCmp.init.__doc__ + ) + assert "Reinitialize an alphabet-aware comparison object" in (RevLenWtLexCmp.init.__doc__) + + @pytest.mark.parametrize( ("alphabet", "x", "y", "missing"), [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], From 50cef20d4bba4355ff8458151c81548daadee145 Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Wed, 19 Aug 2026 15:03:13 +0100 Subject: [PATCH 17/27] order: final doc fixes --- src/libsemigroups_pybind11/order.py | 36 +++++++++++++++++++++++++++++ src/order.cpp | 25 +++++++++++++++++++- tests/test_order.py | 30 ------------------------ 3 files changed, 60 insertions(+), 31 deletions(-) diff --git a/src/libsemigroups_pybind11/order.py b/src/libsemigroups_pybind11/order.py index c5d454fc..f0d1f080 100644 --- a/src/libsemigroups_pybind11/order.py +++ b/src/libsemigroups_pybind11/order.py @@ -452,6 +452,12 @@ def __init__(self: _Self, *args, **kwargs) -> None: def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) + @_copydoc(_WrCmpDefault.init, _WrCmpString.init) + def init(self: _Self, *args) -> _Self: + cxx_self = _to_cxx(self) + result = cxx_self.init(*(_to_cxx(arg) for arg in args)) + return self if result is cxx_self else result + class RevWrCmp(_ConfiguredCmp): __doc__ = _RevWrCmpString.__doc__ @@ -479,6 +485,12 @@ def __init__(self: _Self, *args, **kwargs) -> None: def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) + @_copydoc(_RevWrCmpDefault.init, _RevWrCmpString.init) + def init(self: _Self, *args) -> _Self: + cxx_self = _to_cxx(self) + result = cxx_self.init(*(_to_cxx(arg) for arg in args)) + return self if result is cxx_self else result + class WtLenLexCmp(_ConfiguredCmp): __doc__ = _WtLenLexCmpString.__doc__ @@ -506,6 +518,12 @@ def __init__(self: _Self, *args, **kwargs) -> None: def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) + @_copydoc(_WtLenLexCmpDefault.init, _WtLenLexCmpString.init) + def init(self: _Self, *args) -> _Self: + cxx_self = _to_cxx(self) + result = cxx_self.init(*(_to_cxx(arg) for arg in args)) + return self if result is cxx_self else result + class RevWtLenLexCmp(_ConfiguredCmp): __doc__ = _RevWtLenLexCmpString.__doc__ @@ -533,6 +551,12 @@ def __init__(self: _Self, *args, **kwargs) -> None: def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) + @_copydoc(_RevWtLenLexCmpDefault.init, _RevWtLenLexCmpString.init) + def init(self: _Self, *args) -> _Self: + cxx_self = _to_cxx(self) + result = cxx_self.init(*(_to_cxx(arg) for arg in args)) + return self if result is cxx_self else result + class WtLexCmp(_ConfiguredCmp): __doc__ = _WtLexCmpString.__doc__ @@ -560,6 +584,12 @@ def __init__(self: _Self, *args, **kwargs) -> None: def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) + @_copydoc(_WtLexCmpDefault.init, _WtLexCmpString.init) + def init(self: _Self, *args) -> _Self: + cxx_self = _to_cxx(self) + result = cxx_self.init(*(_to_cxx(arg) for arg in args)) + return self if result is cxx_self else result + class RevWtLexCmp(_ConfiguredCmp): __doc__ = _RevWtLexCmpString.__doc__ @@ -587,6 +617,12 @@ def __init__(self: _Self, *args, **kwargs) -> None: def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: return super().__call__(x, y) + @_copydoc(_RevWtLexCmpDefault.init, _RevWtLexCmpString.init) + def init(self: _Self, *args) -> _Self: + cxx_self = _to_cxx(self) + result = cxx_self.init(*(_to_cxx(arg) for arg in args)) + return self if result is cxx_self else result + class LenWtLexCmp(_ConfiguredCmp): __doc__ = _LenWtLexCmpString.__doc__ diff --git a/src/order.cpp b/src/order.cpp index db59b5f1..3166048f 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -1353,6 +1353,12 @@ Reinitialize an index-word comparison object with an empty {1} vector. :returns: The first argument *self*. :rtype: {0} +.. warning:: + This overload only works if *self* was constructed without an alphabet, as + ``{0}()`` or ``{0}({1})``. An object constructed as + ``{0}(alphabet, {1})`` must be reinitialized with + ``init(alphabet, {1})``. + .. doctest:: python >>> from libsemigroups_pybind11 import {0} @@ -1381,6 +1387,12 @@ Reinitialize an index-word comparison object from a {1} vector. :returns: The first argument *self*. :rtype: {0} +.. warning:: + This overload only works if *self* was constructed without an alphabet, as + ``{0}()`` or ``{0}({1})``. An object constructed as + ``{0}(alphabet, {1})`` must be reinitialized with + ``init(alphabet, {1})``. + .. doctest:: python >>> from libsemigroups_pybind11 import {0} @@ -1524,6 +1536,11 @@ must have the same size. :raises TypeError: if the arguments do not have the required types. +.. warning:: + This overload only works if *self* was constructed as + ``{0}(alphabet, {1})``. An object constructed as ``{0}()`` or + ``{0}({1})`` must be reinitialized with ``init()`` or ``init({1})``. + .. doctest:: python >>> from libsemigroups_pybind11 import Alphabet, {0} @@ -1586,6 +1603,11 @@ Return the stored alphabet. :raises AttributeError: if *self* was constructed without an alphabet. +.. warning:: + This method only works if *self* was constructed as + ``{0}(alphabet, {1})``. An object constructed as ``{0}()`` or + ``{0}({1})`` does not have a stored alphabet. + .. doctest:: python >>> from libsemigroups_pybind11 import Alphabet, {0} @@ -1593,7 +1615,8 @@ Return the stored alphabet. >>> {0}(alphabet, [1, 2]).alphabet() == alphabet True )pbdoc", - name) + name, + configuration) .c_str(), py::return_value_policy::reference_internal); thing.def( diff --git a/tests/test_order.py b/tests/test_order.py index dc910c40..0c79371d 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -488,25 +488,6 @@ def test_len_wt_lex_cmp_object_without_alphabet(): assert compare.weights() == [2, 1] -def test_len_wt_lex_cmp_init_documentation(): - """Check both C++ init variants are included in the Python documentation.""" - assert "Reinitialize an index-word comparison object with an empty" in ( - LenWtLexCmp.init.__doc__ - ) - assert "Reinitialize an index-word comparison object from a weights" in ( - LenWtLexCmp.init.__doc__ - ) - assert "Reinitialize an alphabet-aware comparison object" in (LenWtLexCmp.init.__doc__) - - -def test_len_wt_lex_cmp_accessor_return_documentation(): - """Check accessor return values and types are documented.""" - assert ":returns: The stored alphabet." in LenWtLexCmp.alphabet.__doc__ - assert ":rtype: Alphabet" in LenWtLexCmp.alphabet.__doc__ - assert ":returns: The stored weights." in LenWtLexCmp.weights.__doc__ - assert ":rtype: list[int]" in LenWtLexCmp.weights.__doc__ - - @pytest.mark.parametrize( ("alphabet", "x", "y", "missing"), [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], @@ -556,17 +537,6 @@ def test_rev_len_wt_lex_cmp_object_without_alphabet(): assert compare.weights() == [2, 1] -def test_rev_len_wt_lex_cmp_init_documentation(): - """Check both C++ init variants are included in the Python documentation.""" - assert "Reinitialize an index-word comparison object with an empty" in ( - RevLenWtLexCmp.init.__doc__ - ) - assert "Reinitialize an index-word comparison object from a weights" in ( - RevLenWtLexCmp.init.__doc__ - ) - assert "Reinitialize an alphabet-aware comparison object" in (RevLenWtLexCmp.init.__doc__) - - @pytest.mark.parametrize( ("alphabet", "x", "y", "missing"), [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], From dd53aadca7fef5343a5af5c12c6dde94813d9e41 Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Wed, 19 Aug 2026 15:06:16 +0100 Subject: [PATCH 18/27] Suppress lint warning --- src/libsemigroups_pybind11/order.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/libsemigroups_pybind11/order.py b/src/libsemigroups_pybind11/order.py index f0d1f080..dc3fccf4 100644 --- a/src/libsemigroups_pybind11/order.py +++ b/src/libsemigroups_pybind11/order.py @@ -454,6 +454,7 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: @_copydoc(_WrCmpDefault.init, _WrCmpString.init) def init(self: _Self, *args) -> _Self: + # pylint: disable=missing-function-docstring cxx_self = _to_cxx(self) result = cxx_self.init(*(_to_cxx(arg) for arg in args)) return self if result is cxx_self else result @@ -487,6 +488,7 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: @_copydoc(_RevWrCmpDefault.init, _RevWrCmpString.init) def init(self: _Self, *args) -> _Self: + # pylint: disable=missing-function-docstring cxx_self = _to_cxx(self) result = cxx_self.init(*(_to_cxx(arg) for arg in args)) return self if result is cxx_self else result @@ -520,6 +522,7 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: @_copydoc(_WtLenLexCmpDefault.init, _WtLenLexCmpString.init) def init(self: _Self, *args) -> _Self: + # pylint: disable=missing-function-docstring cxx_self = _to_cxx(self) result = cxx_self.init(*(_to_cxx(arg) for arg in args)) return self if result is cxx_self else result @@ -553,6 +556,7 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: @_copydoc(_RevWtLenLexCmpDefault.init, _RevWtLenLexCmpString.init) def init(self: _Self, *args) -> _Self: + # pylint: disable=missing-function-docstring cxx_self = _to_cxx(self) result = cxx_self.init(*(_to_cxx(arg) for arg in args)) return self if result is cxx_self else result @@ -586,6 +590,7 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: @_copydoc(_WtLexCmpDefault.init, _WtLexCmpString.init) def init(self: _Self, *args) -> _Self: + # pylint: disable=missing-function-docstring cxx_self = _to_cxx(self) result = cxx_self.init(*(_to_cxx(arg) for arg in args)) return self if result is cxx_self else result @@ -619,6 +624,7 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: @_copydoc(_RevWtLexCmpDefault.init, _RevWtLexCmpString.init) def init(self: _Self, *args) -> _Self: + # pylint: disable=missing-function-docstring cxx_self = _to_cxx(self) result = cxx_self.init(*(_to_cxx(arg) for arg in args)) return self if result is cxx_self else result @@ -652,6 +658,7 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: @_copydoc(_LenWtLexCmpDefault.init, _LenWtLexCmpString.init) def init(self: _Self, *args) -> _Self: + # pylint: disable=missing-function-docstring cxx_self = _to_cxx(self) result = cxx_self.init(*(_to_cxx(arg) for arg in args)) return self if result is cxx_self else result @@ -685,6 +692,7 @@ def __call__(self: _Self, x: str | list[int], y: str | list[int]) -> bool: @_copydoc(_RevLenWtLexCmpDefault.init, _RevLenWtLexCmpString.init) def init(self: _Self, *args) -> _Self: + # pylint: disable=missing-function-docstring cxx_self = _to_cxx(self) result = cxx_self.init(*(_to_cxx(arg) for arg in args)) return self if result is cxx_self else result From 427d27666ee7f17af69d8ced3b4b225917fd39d1 Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Thu, 20 Aug 2026 08:58:18 +0100 Subject: [PATCH 19/27] Updates from code review --- .../data-structures/order/functions.rst | 7 +- docs/source/data-structures/order/index.rst | 7 +- src/order.cpp | 114 ++++++------------ tests/test_order.py | 45 +++++-- 4 files changed, 83 insertions(+), 90 deletions(-) diff --git a/docs/source/data-structures/order/functions.rst b/docs/source/data-structures/order/functions.rst index db23523c..032175a6 100644 --- a/docs/source/data-structures/order/functions.rst +++ b/docs/source/data-structures/order/functions.rst @@ -1,5 +1,5 @@ .. - Copyright (c) 2024-2026 Joseph Edwards, J. D. Mitchell + Copyright (c) 2026 Joseph Edwards, J. D. Mitchell Distributed under the terms of the GPL license version 3. @@ -10,8 +10,9 @@ Functions for orders ==================== -This page contains the documentation for functions that compare words or -strings with respect to certain reduction orderings. +This page contains the documentation for functions that compare lists of +integers ``list[int]`` or strings ``str`` with respect to certain reduction +orderings. .. important:: diff --git a/docs/source/data-structures/order/index.rst b/docs/source/data-structures/order/index.rst index b3196b17..8a8449d0 100644 --- a/docs/source/data-structures/order/index.rst +++ b/docs/source/data-structures/order/index.rst @@ -11,15 +11,16 @@ Orders ====== This page contains the documentation for classes and several functions for -comparing words or strings with respect to certain reduction orderings. +comparing words (i.e. lists of integers ``list[int]`` or strings ``str``) with +respect to certain reduction orderings. .. seealso:: - :any:`Order` - :any:`Alphabet` -The classes and functions in ``libsemigroups_pybind11`` for ordering words and -strings are described on the following pages: +The classes and functions in ``libsemigroups_pybind11`` for comparing words +are described on the following pages: .. toctree:: :maxdepth: 1 diff --git a/src/order.cpp b/src/order.cpp index 3166048f..4225852b 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -143,7 +143,7 @@ Words are first ordered by length and then lexicographically. :returns: Whether *x* is less than *y*. :rtype: bool -.. seealso:: :any:`LenLexCmp` for a reusable len-lex comparison object. +.. seealso:: :any:`LenLexCmp` for a reusable lenlex comparison object. .. doctest:: python @@ -1325,7 +1325,7 @@ Construct a comparison object with an empty {1} vector. py::arg(configuration.c_str()), fmt::format(R"pbdoc( :sig=(self: {0}, {1}: list[int]) -> None: -Construct a comparison object for index words. +Construct a comparison object for ``list[int]`` words. :param {1}: the {1} of the generators. :type {1}: list[int] @@ -1343,34 +1343,6 @@ Construct a comparison object for index words. [](Cmp const& self) { return to_human_readable_repr(self); }); thing.def("__copy__", [](Cmp const& self) { return Cmp(self); }); thing.def("copy", [](Cmp const& self) { return Cmp(self); }); - thing.def( - "init", - [](Cmp& self) -> Cmp& { return self.init(); }, - fmt::format(R"pbdoc( -:sig=(self: {0}) -> {0}: -Reinitialize an index-word comparison object with an empty {1} vector. - -:returns: The first argument *self*. -:rtype: {0} - -.. warning:: - This overload only works if *self* was constructed without an alphabet, as - ``{0}()`` or ``{0}({1})``. An object constructed as - ``{0}(alphabet, {1})`` must be reinitialized with - ``init(alphabet, {1})``. - -.. doctest:: python - - >>> from libsemigroups_pybind11 import {0} - >>> compare = {0}([1, 2]) - >>> compare.init() is compare - True - >>> compare.{1}() - [] -)pbdoc", - name, - configuration) - .c_str()); thing.def( "init", [](Cmp& self, std::vector const& values) -> Cmp& { @@ -1379,7 +1351,7 @@ Reinitialize an index-word comparison object with an empty {1} vector. py::arg(configuration.c_str()), fmt::format(R"pbdoc( :sig=(self: {0}, {1}: list[int]) -> {0}: -Reinitialize an index-word comparison object from a {1} vector. +Reinitialize a {0} object from a {1} list. :param {1}: the {1} of the generators. :type {1}: list[int] @@ -1430,7 +1402,7 @@ Reinitialize an index-word comparison object from a {1} vector. fmt::format(R"pbdoc( Compare words using {2}. -Use ``{0}({1})`` to compare ``list[int]`` index words, where the entries of +Use ``{0}({1})`` to compare ``list[int]`` words, where the entries of *{1}* correspond to the indices. Use ``{0}(alphabet, {1})`` to compare words whose letters belong to *alphabet*. The latter form copies both arguments and only accepts words with the same type as *alphabet*. @@ -1520,8 +1492,8 @@ Copy a comparison object. :sig=(self: {0}, alphabet: Alphabet, {1}: list[int]) -> {0}: Reinitialize an alphabet-aware comparison object. -The alphabet must have the original word type, and the alphabet and {1} vector -must have the same size. +The alphabet must have the original word type (i.e. ``list[int]`` or ``str``), +and the alphabet and {1} list must have the same size. :param alphabet: the alphabet defining the letters and their order. :type alphabet: Alphabet @@ -1539,7 +1511,7 @@ must have the same size. .. warning:: This overload only works if *self* was constructed as ``{0}(alphabet, {1})``. An object constructed as ``{0}()`` or - ``{0}({1})`` must be reinitialized with ``init()`` or ``init({1})``. + ``{0}({1})`` must be reinitialized with ``init({1})``. .. doctest:: python @@ -1563,8 +1535,8 @@ must have the same size. :sig=(self: {0}, x: str | list[int], y: str | list[int]) -> bool: Compare two words using {2}. -Index-word objects accept ``list[int]``. Alphabet-aware objects accept words -of the same type as their alphabet. +Non-alphabet aware objects accept ``list[int]``. Alphabet-aware objects accept +words of the same type as their alphabet. :param x: the first word. :type x: str | list[int] @@ -1659,9 +1631,8 @@ the positions of their letters in *alphabet*. The latter form copies .. note:: The constructor fixes whether this object is alphabet-aware (i.e. constructed from an :any:`Alphabet` object). It also fixes the word type of an - alphabet-aware object. In particular, ``LexCmp()`` only accepts ``init()`` - (which does nothing); it cannot be changed into an alphabet-aware object by - calling ``init(alphabet)``. Similarly, + alphabet-aware object. In particular, ``LexCmp()`` cannot be changed into an + alphabet-aware object by calling ``init(alphabet)``. Similarly, ``LexCmp(alphabet).init(new_alphabet)`` requires *new_alphabet* to have the same word type as *alphabet*. @@ -1844,7 +1815,7 @@ Return the alphabet used to compare letters. // The Python wrapper copies all documentation from this // specialization, except those given above. py::class_ thing(m, name, R"pbdoc( -Compare words using len-lex ordering. +Compare words using lenlex ordering. Words are first compared by length and then lexicographically. Use ``LenLexCmp()`` to compare either ``str`` or ``list[int]`` words using the @@ -1855,9 +1826,8 @@ form copies *alphabet* and only accepts words with the same type as *alphabet*. .. note:: The constructor fixes whether this object is alphabet-aware (i.e. constructed from an :any:`Alphabet` object). It also fixes the word type of an - alphabet-aware object. In particular, ``LenLexCmp()`` only accepts - ``init()`` (which does nothing); it cannot be changed into an alphabet-aware - object by calling ``init(alphabet)``. Similarly, + alphabet-aware object. In particular, ``LenLexCmp()`` cannot be changed into + an alphabet-aware object by calling ``init(alphabet)``. Similarly, ``LenLexCmp(alphabet).init(new_alphabet)`` requires *new_alphabet* to have the same word type as *alphabet*. @@ -1880,7 +1850,7 @@ form copies *alphabet* and only accepts words with the same type as *alphabet*. py::arg("alphabet"), R"pbdoc( :sig=(self: LenLexCmp, alphabet: Alphabet) -> None: -Construct a len-lex comparison object from an alphabet. +Construct a lenlex comparison object from an alphabet. Constructs an object whose call operator first compares words by length and then compares words of equal length by the positions of their letters in @@ -1978,7 +1948,7 @@ had it been newly constructed from *alphabet*. py::arg("y"), R"pbdoc( :sig=(self: LenLexCmp, x: str | list[int], y: str | list[int]) -> bool: -Compare two words using len-lex ordering. +Compare two words using lenlex ordering. Words are first compared by length and then lexicographically. If *self* was constructed as ``LenLexCmp()``, then *x* and *y* must either both be strings or @@ -2054,9 +2024,8 @@ the positions of their letters in *alphabet*. The latter form copies .. note:: The constructor fixes whether this object is alphabet-aware (i.e. constructed from an :any:`Alphabet` object). It also fixes the word type of an - alphabet-aware object. In particular, ``RPOCmp()`` only accepts ``init()`` - (which does nothing); it cannot be changed into an alphabet-aware object by - calling ``init(alphabet)``. Similarly, + alphabet-aware object. In particular, ``RPOCmp()`` cannot be changed into an + alphabet-aware object by calling ``init(alphabet)``. Similarly, ``RPOCmp(alphabet).init(new_alphabet)`` requires *new_alphabet* to have the same word type as *alphabet*. @@ -2259,9 +2228,8 @@ copies *alphabet* and only accepts words with the same type as *alphabet*. .. note:: The constructor fixes whether this object is alphabet-aware (i.e. constructed from an :any:`Alphabet` object). It also fixes the word type of an - alphabet-aware object. In particular, ``RevRPOCmp()`` only accepts - ``init()`` (which does nothing); it cannot be changed into an alphabet-aware - object by calling ``init(alphabet)``. Similarly, + alphabet-aware object. In particular, ``RevRPOCmp()`` cannot be changed into + an alphabet-aware object by calling ``init(alphabet)``. Similarly, ``RevRPOCmp(alphabet).init(new_alphabet)`` requires *new_alphabet* to have the same word type as *alphabet*. @@ -2469,9 +2437,8 @@ the positions of their letters in *alphabet*. The latter form copies .. note:: The constructor fixes whether this object is alphabet-aware (i.e. constructed from an :any:`Alphabet` object). It also fixes the word type of an - alphabet-aware object. In particular, ``RevLexCmp()`` only accepts ``init()`` - (which does nothing); it cannot be changed into an alphabet-aware object by - calling ``init(alphabet)``. Similarly, + alphabet-aware object. In particular, ``RevLexCmp()`` cannot be changed into + an alphabet-aware object by calling ``init(alphabet)``. Similarly, ``RevLexCmp(alphabet).init(new_alphabet)`` requires *new_alphabet* to have the same word type as *alphabet*. @@ -2658,7 +2625,7 @@ Return the alphabet used to compare letters. // The Python wrapper copies all documentation from this specialization, // except those given above. py::class_ thing(m, name, R"pbdoc( -Compare words using reversed len-lex ordering. +Compare words using reversed lenlex ordering. Words are first compared by length and then lexicographically after being read from right to left. Use ``RevLenLexCmp()`` to compare either ``str`` or @@ -2670,9 +2637,8 @@ words with the same type as *alphabet*. .. note:: The constructor fixes whether this object is alphabet-aware (i.e. constructed from an :any:`Alphabet` object). It also fixes the word type of an - alphabet-aware object. In particular, ``RevLenLexCmp()`` only accepts - ``init()`` (which does nothing); it cannot be changed into an alphabet-aware - object by calling ``init(alphabet)``. Similarly, + alphabet-aware object. In particular, ``RevLenLexCmp()`` cannot be changed + into an alphabet-aware object by calling ``init(alphabet)``. Similarly, ``RevLenLexCmp(alphabet).init(new_alphabet)`` requires *new_alphabet* to have the same word type as *alphabet*. @@ -2695,7 +2661,7 @@ words with the same type as *alphabet*. py::arg("alphabet"), R"pbdoc( :sig=(self: RevLenLexCmp, alphabet: Alphabet) -> None: -Construct a reversed len-lex comparison object from an alphabet. +Construct a reversed lenlex comparison object from an alphabet. Constructs an object whose call operator first compares words by length and then compares words of equal length from right to left by the positions of @@ -2792,7 +2758,7 @@ had it been newly constructed from *alphabet*. py::arg("y"), R"pbdoc( :sig=(self: RevLenLexCmp, x: str | list[int], y: str | list[int]) -> bool: -Compare two words using reversed len-lex ordering. +Compare two words using reversed lenlex ordering. Words are first compared by length and then lexicographically after being read from right to left. If *self* was constructed as ``RevLenLexCmp()``, then *x* @@ -2850,14 +2816,14 @@ Return the alphabet used to compare letters. )pbdoc", py::return_value_policy::reference_internal); } // bind_rev_lenlex_cmp_with_alphabet - } // namespace + } // namespace void init_order(py::module& m) { py::options options; options.disable_enum_members_docstring(); py::enum_(m, "Order", R"pbdoc( -An enum class for the possible orderings of words and strings. +An enum class for the possible orderings of lists of integers and strings. The values in this enum can be used as the arguments for functions such as :any:`ToddCoxeter.standardize` or :any:`WordRange.order` to specify which @@ -2868,18 +2834,18 @@ backwards compatibility; use :any:`Order.lenlex` and :any:`Order.rev_rpo`, respectively, in new code. .. py:attribute:: Order.none - :value: + :value: No ordering .. py:attribute:: Order.lenlex - :value: + :value: The lenlex ordering. Words are first ordered by length, and then lexicographically. .. py:attribute:: Order.shortlex - :value: + :value: The short-lex ordering. Words are first ordered by length, and then lexicographically. @@ -2887,27 +2853,27 @@ respectively, in new code. This is deprecated; use :any:`Order.lenlex` instead. .. py:attribute:: Order.lex - :value: + :value: The lexicographic ordering. Note that this is not a well-order, so there may not be a lexicographically least word in a given congruence class of words. .. py:attribute:: Order.rpo - :value: + :value: The recursive-path ordering, as described in :cite:`Jantzen2012aa` (Definition 1.2.14, page 24). .. py:attribute:: Order.rev_rpo - :value: + :value: The reversed recursive-path ordering, based on the description in :cite:`Jantzen2012aa` (Definition 1.2.14, page 24), where words are read right-to-left before ordering. .. py:attribute:: Order.recursive - :value: + :value: The recursive-path ordering, as described in :cite:`Jantzen2012aa` (Definition 1.2.14, page 24). @@ -2999,14 +2965,14 @@ respectively, in new code. "WtLenLexCmpString", "WtLenLexCmp", "weights", - "weighted len-lex ordering", + "weighted lenlex ordering", &WtLenLexCmp::weights); bind_configured_cmp_with_alphabet, word_type>( m, "WtLenLexCmpWord", "WtLenLexCmp", "weights", - "weighted len-lex ordering", + "weighted lenlex ordering", &WtLenLexCmp::weights); bind_configured_cmp_default>( @@ -3016,14 +2982,14 @@ respectively, in new code. "RevWtLenLexCmpString", "RevWtLenLexCmp", "weights", - "reversed weighted len-lex ordering", + "reversed weighted lenlex ordering", &RevWtLenLexCmp::weights); bind_configured_cmp_with_alphabet, word_type>( m, "RevWtLenLexCmpWord", "RevWtLenLexCmp", "weights", - "reversed weighted len-lex ordering", + "reversed weighted lenlex ordering", &RevWtLenLexCmp::weights); bind_configured_cmp_default>( diff --git a/tests/test_order.py b/tests/test_order.py index 0c79371d..e561e40e 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -57,7 +57,7 @@ def test_lex_cmp_object_without_alphabet(): def test_rev_wr_cmp_object_without_alphabet(): - """Check reversed wreath-product comparison of index words.""" + """Check reversed wreath-product comparison of list[int].""" compare = RevWrCmp([1, 2]) assert compare.levels() == [1, 2] assert compare([0], [1]) @@ -284,7 +284,7 @@ def test_rev_rpo_cmp_object_rejects_bad_constructors(): def test_wr_cmp_object_without_alphabet(): - """Check wreath-product comparison of index words.""" + """Check wreath-product comparison of list[int].""" compare = WrCmp([1, 2]) assert compare.levels() == [1, 2] assert compare([0], [1]) @@ -333,7 +333,7 @@ def test_wr_cmp_object_rejects_bad_constructors(): def test_wt_lenlex_cmp_object_without_alphabet(): - """Check weighted len-lex comparison of index words.""" + """Check weighted lenlex comparison of list[int].""" compare = WtLenLexCmp([1, 2]) assert compare.weights() == [1, 2] assert compare([0], [1]) @@ -346,7 +346,7 @@ def test_wt_lenlex_cmp_object_without_alphabet(): [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], ) def test_wt_lenlex_cmp_object_with_alphabet(alphabet, x, y, missing): - """Check alphabet-aware weighted len-lex comparison.""" + """Check alphabet-aware weighted lenlex comparison.""" compare = WtLenLexCmp(alphabet, [1, 2]) assert compare(x, y) assert compare.alphabet() == alphabet @@ -382,7 +382,7 @@ def test_wt_lenlex_cmp_object_rejects_bad_constructors(): def test_rev_wt_lenlex_cmp_object_without_alphabet(): - """Check reversed weighted len-lex comparison of index words.""" + """Check reversed weighted lenlex comparison of list[int].""" compare = RevWtLenLexCmp([1, 2]) assert compare.weights() == [1, 2] assert compare([0], [1]) @@ -395,7 +395,7 @@ def test_rev_wt_lenlex_cmp_object_without_alphabet(): [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], ) def test_rev_wt_lenlex_cmp_object_with_alphabet(alphabet, x, y, missing): - """Check alphabet-aware reversed weighted len-lex comparison.""" + """Check alphabet-aware reversed weighted lenlex comparison.""" compare = RevWtLenLexCmp(alphabet, [1, 2]) assert compare(x, y) assert compare.alphabet() == alphabet @@ -431,7 +431,7 @@ def test_rev_wt_lenlex_cmp_object_rejects_bad_constructors(): def test_wt_lex_cmp_object_without_alphabet(): - """Check weighted lexicographic comparison of index words.""" + """Check weighted lexicographic comparison of list[int].""" compare = WtLexCmp([1, 2]) assert compare.weights() == [1, 2] assert compare([0], [1]) @@ -480,7 +480,7 @@ def test_wt_lex_cmp_object_rejects_bad_constructors(): def test_len_wt_lex_cmp_object_without_alphabet(): - """Check length then weighted lexicographic comparison of index words.""" + """Check length then weighted lexicographic comparison of list[int].""" compare = LenWtLexCmp([1, 2]) assert compare.weights() == [1, 2] assert compare([0], [1]) @@ -529,7 +529,7 @@ def test_len_wt_lex_cmp_object_rejects_bad_constructors(): def test_rev_len_wt_lex_cmp_object_without_alphabet(): - """Check length then reversed weighted lexicographic comparison of index words.""" + """Check length then reversed weighted lexicographic comparison of list[int].""" compare = RevLenWtLexCmp([1, 2]) assert compare.weights() == [1, 2] assert compare([0], [1]) @@ -578,7 +578,7 @@ def test_rev_len_wt_lex_cmp_object_rejects_bad_constructors(): def test_rev_wt_lex_cmp_object_without_alphabet(): - """Check reversed weighted lexicographic comparison of index words.""" + """Check reversed weighted lexicographic comparison of list[int].""" compare = RevWtLexCmp([1, 2]) assert compare.weights() == [1, 2] assert compare([0], [1]) @@ -626,6 +626,31 @@ def test_rev_wt_lex_cmp_object_rejects_bad_constructors(): RevWtLexCmp(Alphabet("ab"), [1]) +@pytest.mark.parametrize( + "comparison_type", + [ + LexCmp, + RevLexCmp, + LenLexCmp, + RevLenLexCmp, + RPOCmp, + RevRPOCmp, + WrCmp, + RevWrCmp, + WtLenLexCmp, + RevWtLenLexCmp, + WtLexCmp, + RevWtLexCmp, + LenWtLexCmp, + RevLenWtLexCmp, + ], +) +def test_comparison_object_init_requires_an_argument(comparison_type): + """Check comparison objects have no zero-argument init overload.""" + with pytest.raises((AttributeError, TypeError)): + comparison_type().init() + + @pytest.mark.parametrize( "comparison_type", [LexCmp, RevLexCmp, LenLexCmp, RevLenLexCmp, RPOCmp, RevRPOCmp] ) From 80961a08e6a282bb5516289372a2ab925f17ffbb Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Thu, 20 Aug 2026 09:12:11 +0100 Subject: [PATCH 20/27] order: doc further improve the wording --- src/order.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/order.cpp b/src/order.cpp index 4225852b..ec382949 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -1547,9 +1547,9 @@ words of the same type as their alphabet. :rtype: bool :raises TypeError: - if the word types do not match the construction mode. + if the word types do not match the type used when the object was constructed. :raises LibsemigroupsError: - if a word contains a letter not represented by the stored configuration. + if a word contains a letter not represented by the stored weights or levels. .. doctest:: python @@ -1687,7 +1687,7 @@ for the call operator. :sig=(self: LexCmp) -> LexCmp: Copy a comparison object. -The copy has the same mode and word type as *self*. For an alphabet-aware +The copy has the same word type as *self*; and for an alphabet-aware object, the stored alphabet is also copied, so subsequently reinitializing one comparison object does not affect the other. @@ -1884,7 +1884,7 @@ type for the call operator. :sig=(self: LenLexCmp) -> LenLexCmp: Copy a comparison object. -The copy has the same mode and word type as *self*. For an alphabet-aware +The copy has the same word type as *self*; and for an alphabet-aware object, the stored alphabet is also copied, so subsequently reinitializing one comparison object does not affect the other. @@ -2084,7 +2084,7 @@ letters in *alphabet* also fixes the accepted word type for the call operator. :sig=(self: RPOCmp) -> RPOCmp: Copy a comparison object. -The copy has the same mode and word type as *self*. For an alphabet-aware +The copy has the same word type as *self*; and for an alphabet-aware object, the stored alphabet is also copied, so subsequently reinitializing one comparison object does not affect the other. @@ -2290,7 +2290,7 @@ call operator. :sig=(self: RevRPOCmp) -> RevRPOCmp: Copy a comparison object. -The copy has the same mode and word type as *self*. For an alphabet-aware +The copy has the same word type as *self*; and for an alphabet-aware object, the stored alphabet is also copied, so subsequently reinitializing one comparison object does not affect the other. @@ -2494,7 +2494,7 @@ for the call operator. :sig=(self: RevLexCmp) -> RevLexCmp: Copy a comparison object. -The copy has the same mode and word type as *self*. For an alphabet-aware +The copy has the same word type as *self*; and for an alphabet-aware object, the stored alphabet is also copied, so subsequently reinitializing one comparison object does not affect the other. @@ -2695,7 +2695,7 @@ accepted word type for the call operator. :sig=(self: RevLenLexCmp) -> RevLenLexCmp: Copy a comparison object. -The copy has the same mode and word type as *self*. For an alphabet-aware +The copy has the same word type as *self*; and for an alphabet-aware object, the stored alphabet is also copied, so subsequently reinitializing one comparison object does not affect the other. From fde8a42d30d263725dfa11526fe8d9c3a58eee01 Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Thu, 20 Aug 2026 09:13:51 +0100 Subject: [PATCH 21/27] order: remove unused header --- src/order.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/order.cpp b/src/order.cpp index ec382949..b15db0e2 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -17,9 +17,8 @@ // // C++ stl headers.... -#include // for string -#include // for is_same_v -#include // for vector +#include // for string +#include // for vector // libsemigroups.... #include // for *_cmp, Order From 767976a715841189ae94f2a0496699886d4df532 Mon Sep 17 00:00:00 2001 From: Codex OpenAI Date: Thu, 20 Aug 2026 09:24:31 +0100 Subject: [PATCH 22/27] order: reorganise tests --- tests/test_order.py | 1489 +++++++++++++++++++++++++------------------ 1 file changed, 854 insertions(+), 635 deletions(-) diff --git a/tests/test_order.py b/tests/test_order.py index e561e40e..50910b87 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -48,837 +48,1056 @@ ) -def test_lex_cmp_object_without_alphabet(): - """Check the stateless comparison object with both word types.""" - compare = LexCmp() - assert compare("a", "b") - assert compare([0], [1]) - assert not compare("b", "aa") - - -def test_rev_wr_cmp_object_without_alphabet(): - """Check reversed wreath-product comparison of list[int].""" - compare = RevWrCmp([1, 2]) - assert compare.levels() == [1, 2] - assert compare([0], [1]) - assert compare.init([2, 1]) is compare - assert compare.levels() == [2, 1] - - -@pytest.mark.parametrize( - ("alphabet", "x", "y", "missing"), - [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], -) -def test_rev_wr_cmp_object_with_alphabet(alphabet, x, y, missing): - """Check alphabet-aware reversed wreath-product comparison.""" - compare = RevWrCmp(alphabet, [1, 2]) - assert compare(x, y) - assert compare.alphabet() == alphabet - assert compare.levels() == [1, 2] - assert compare.init(alphabet, [1, 2]) is compare +def _check_simple_copy_and_repr(Cmp, name): + """Check copies of alphabet-free and alphabet-aware objects.""" + for original, alphabet, replacement, x, y in ( + (Cmp(), None, None, "a", "b"), + (Cmp(Alphabet("ba")), Alphabet("ba"), Alphabet("ab"), "b", "a"), + (Cmp(Alphabet([1, 0])), Alphabet([1, 0]), Alphabet([0, 1]), [1], [0]), + ): + copies = ( + original.copy(), + original.__copy__(), # pylint: disable=unnecessary-dunder-call + copy(original), + ) + assert all(type(copied) is Cmp for copied in copies) + assert all(copied is not original for copied in copies) + assert all( + copied.py_template_params == original.py_template_params + for copied in copies + ) + assert all(copied(x, y) for copied in copies) + if alphabet is not None: + assert all(copied.alphabet() == alphabet for copied in copies) + + if replacement is not None: + original.init(replacement) + assert not original(x, y) + assert all(copied(x, y) for copied in copies) + + assert repr(Cmp()) == f"<{name} object>" + assert repr(Cmp(Alphabet("ba"))) == (f'<{name} object over >') + assert repr(Cmp(Alphabet([1, 0]))) == (f"<{name} object over >") + + +def _check_configured_copy_and_repr(Cmp, name, configuration_name, accessor_name): + """Check copies of configured alphabet-free and alphabet-aware objects.""" + for original, alphabet, replacement_args in ( + (Cmp([1, 2]), None, ([2, 1],)), + (Cmp(Alphabet("ab"), [1, 2]), Alphabet("ab"), (Alphabet("ab"), [2, 1])), + (Cmp(Alphabet([0, 1]), [1, 2]), Alphabet([0, 1]), (Alphabet([0, 1]), [2, 1])), + ): + copies = ( + original.copy(), + original.__copy__(), # pylint: disable=unnecessary-dunder-call + copy(original), + ) + original.init(*replacement_args) + assert all(type(copied) is Cmp for copied in copies) + assert all(copied is not original for copied in copies) + assert all(getattr(copied, accessor_name)() == [1, 2] for copied in copies) + if alphabet is not None: + assert all(copied.alphabet() == alphabet for copied in copies) + assert getattr(original, accessor_name)() == [2, 1] + + assert repr(Cmp([1, 2])) == (f"<{name} object with {configuration_name} [1, 2]>") + assert repr(Cmp(Alphabet("ab"), [1, 2])) == ( + f'<{name} object over with {configuration_name} [1, 2]>' + ) + assert repr(Cmp(Alphabet([0, 1]), [1, 2])) == ( + f"<{name} object over with {configuration_name} [1, 2]>" + ) + + +def _check_simple_bad_constructor_arguments(Cmp): + """Check constructor and zero-argument init validation.""" + with pytest.raises(TypeError): + Cmp("abc") + with pytest.raises(TypeError): + Cmp(Alphabet("abc"), None) + with pytest.raises((AttributeError, TypeError)): + Cmp().init() + + +def _check_configured_bad_constructor_arguments(Cmp): + """Check constructor and zero-argument init validation.""" + with pytest.raises(TypeError): + Cmp(Alphabet("ab")) + with pytest.raises(LibsemigroupsError): + Cmp(Alphabet("ab"), [1]) + with pytest.raises((AttributeError, TypeError)): + Cmp().init() + +############################################################################### +# Lex order +############################################################################### + + +def test_lex_without_alphabet(): + """Check lexicographic comparison without an alphabet.""" + assert lex_cmp("ab", "ba") + assert lex_cmp([0, 1], [1, 0]) + + cmp = LexCmp() + assert cmp("a", "b") + assert cmp([0], [1]) + assert not cmp("b", "aa") + + +def test_lex_with_alphabet(): + """Check lexicographic comparison with an alphabet.""" + alphabet = Alphabet("cba") + x = "cba" + y = "cab" + assert lex_cmp(alphabet, x, y) + + cmp = LexCmp(alphabet=alphabet) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.init(alphabet) is cmp + + with pytest.raises(LibsemigroupsError): + lex_cmp(alphabet, x, "d") with pytest.raises(LibsemigroupsError): - compare(x, missing) + cmp(x, "d") + alphabet = Alphabet([4, 2, 7, 1]) + x = [4, 7] + y = [2, 4] + assert lex_cmp(alphabet, x, y) -def test_rev_wr_cmp_object_copy_and_repr(): + cmp = LexCmp(alphabet) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.init(alphabet) is cmp + + with pytest.raises(LibsemigroupsError): + lex_cmp(alphabet, x, [9]) + with pytest.raises(LibsemigroupsError): + cmp(x, [9]) + + +def test_lex_object_copy_and_repr(): """Check copies and human-readable representations.""" - original = RevWrCmp(Alphabet("ab"), [1, 2]) - copies = ( - original.copy(), - original.__copy__(), # pylint: disable=unnecessary-dunder-call - copy(original), - ) - original.init(Alphabet("ab"), [2, 1]) - assert all(copied.levels() == [1, 2] for copied in copies) - assert repr(RevWrCmp([1, 2])) == "" - assert repr(RevWrCmp(Alphabet("ab"), [1, 2])) == ( - ' with levels [1, 2]>' - ) + _check_simple_copy_and_repr(LexCmp, "LexCmp") -def test_rev_wr_cmp_object_rejects_bad_constructors(): +def test_lex_object_rejects_bad_constructor_arguments(): """Check constructor validation.""" - with pytest.raises(TypeError): - RevWrCmp(Alphabet("ab")) + _check_simple_bad_constructor_arguments(LexCmp) + + +############################################################################### +# RevLex order +############################################################################### + + +def test_rev_lex_without_alphabet(): + """Check reverse lexicographic comparison without an alphabet.""" + assert rev_lex_cmp("ba", "ab") + assert rev_lex_cmp([1, 0], [0, 1]) + + cmp = RevLexCmp() + assert cmp("a", "b") + assert cmp([0], [1]) + assert cmp("ba", "ab") + + +def test_rev_lex_with_alphabet(): + """Check reverse lexicographic comparison with an alphabet.""" + alphabet = Alphabet("qpr") + x = "rq" + y = "rp" + assert rev_lex_cmp(alphabet, x, y) + + cmp = RevLexCmp(alphabet=alphabet) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.init(alphabet) is cmp + with pytest.raises(LibsemigroupsError): - RevWrCmp(Alphabet("ab"), [1]) + rev_lex_cmp(alphabet, x, "s") + with pytest.raises(LibsemigroupsError): + cmp(x, "s") + alphabet = Alphabet([8, 3, 5]) + x = [5, 8] + y = [5, 3] + assert rev_lex_cmp(alphabet, x, y) -@pytest.mark.parametrize( - ("alphabet", "x", "y", "missing"), - [(Alphabet("ba"), "b", "a", "c"), (Alphabet([1, 0]), [1], [0], [2])], -) -def test_lex_cmp_object_with_alphabet(alphabet, x, y, missing): - """Check that the comparison object stores and uses its alphabet.""" - compare = LexCmp(alphabet=alphabet) - assert compare(x, y) - assert compare.alphabet() == alphabet - assert compare.init(alphabet) is compare + cmp = RevLexCmp(alphabet) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.init(alphabet) is cmp with pytest.raises(LibsemigroupsError): - compare(x, missing) + rev_lex_cmp(alphabet, x, [9]) + with pytest.raises(LibsemigroupsError): + cmp(x, [9]) -def test_lex_cmp_object_rejects_bad_constructors(): - """Check that only zero arguments or one Alphabet are accepted.""" - with pytest.raises(TypeError): - LexCmp("abc") - with pytest.raises(TypeError): - LexCmp(Alphabet("abc"), Alphabet("abc")) +def test_rev_lex_object_copy_and_repr(): + """Check copies and human-readable representations.""" + _check_simple_copy_and_repr(RevLexCmp, "RevLexCmp") -def test_rev_lex_cmp_object_without_alphabet(): - """Check the stateless comparison object with both word types.""" - compare = RevLexCmp() - assert compare("a", "b") - assert compare([0], [1]) - assert compare("ba", "ab") +def test_rev_lex_object_rejects_bad_constructor_arguments(): + """Check constructor validation.""" + _check_simple_bad_constructor_arguments(RevLexCmp) -@pytest.mark.parametrize( - ("alphabet", "x", "y", "missing"), - [(Alphabet("ba"), "b", "a", "c"), (Alphabet([1, 0]), [1], [0], [2])], -) -def test_rev_lex_cmp_object_with_alphabet(alphabet, x, y, missing): - """Check that the comparison object stores and uses its alphabet.""" - compare = RevLexCmp(alphabet=alphabet) - assert compare(x, y) - assert compare.alphabet() == alphabet - assert compare.init(alphabet) is compare +############################################################################### +# LenLex order +############################################################################### - with pytest.raises(LibsemigroupsError): - compare(x, missing) +def test_lenlex_without_alphabet(): + """Check length-lexicographic comparison without an alphabet.""" + assert lenlex_cmp("ba", "aaa") + assert lenlex_cmp([1, 0], [0, 0, 0]) -def test_rev_lex_cmp_object_rejects_bad_constructors(): - """Check that only zero arguments or one Alphabet are accepted.""" - with pytest.raises(TypeError): - RevLexCmp("abc") - with pytest.raises(TypeError): - RevLexCmp(Alphabet("abc"), Alphabet("abc")) + cmp = LenLexCmp() + assert cmp("a", "b") + assert cmp([0], [1]) + assert not cmp("bb", "a") -def test_lenlex_cmp_object_without_alphabet(): - """Check the stateless comparison object with both word types.""" - compare = LenLexCmp() - assert compare("a", "b") - assert compare([0], [1]) - assert not compare("bb", "a") +def test_lenlex_with_alphabet(): + """Check length-lexicographic comparison with an alphabet.""" + alphabet = Alphabet("yxz") + x = "zy" + y = "zx" + assert lenlex_cmp(alphabet, x, y) + cmp = LenLexCmp(alphabet=alphabet) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.init(alphabet) is cmp -@pytest.mark.parametrize( - ("alphabet", "x", "y", "missing"), - [(Alphabet("ba"), "b", "a", "c"), (Alphabet([1, 0]), [1], [0], [2])], -) -def test_lenlex_cmp_object_with_alphabet(alphabet, x, y, missing): - """Check that the comparison object stores and uses its alphabet.""" - compare = LenLexCmp(alphabet=alphabet) - assert compare(x, y) - assert compare.alphabet() == alphabet - assert compare.init(alphabet) is compare + with pytest.raises(LibsemigroupsError): + lenlex_cmp(alphabet, x, "w") + with pytest.raises(LibsemigroupsError): + cmp(x, "w") + + alphabet = Alphabet([6, 2, 9]) + x = [9, 6] + y = [9, 2] + assert lenlex_cmp(alphabet, x, y) + cmp = LenLexCmp(alphabet) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.init(alphabet) is cmp + + with pytest.raises(LibsemigroupsError): + lenlex_cmp(alphabet, x, [4]) with pytest.raises(LibsemigroupsError): - compare(x, missing) + cmp(x, [4]) -def test_lenlex_cmp_object_rejects_bad_constructors(): - """Check that only zero arguments or one Alphabet are accepted.""" - with pytest.raises(TypeError): - LenLexCmp("abc") - with pytest.raises(TypeError): - LenLexCmp(Alphabet("abc"), Alphabet("abc")) +def test_lenlex_object_copy_and_repr(): + """Check copies and human-readable representations.""" + _check_simple_copy_and_repr(LenLexCmp, "LenLexCmp") -def test_rev_lenlex_cmp_object_without_alphabet(): - """Check the stateless comparison object with both word types.""" - compare = RevLenLexCmp() - assert compare("a", "b") - assert compare([0], [1]) - assert compare("ba", "ab") +def test_lenlex_object_rejects_bad_constructor_arguments(): + """Check constructor validation.""" + _check_simple_bad_constructor_arguments(LenLexCmp) -@pytest.mark.parametrize( - ("alphabet", "x", "y", "missing"), - [(Alphabet("ba"), "b", "a", "c"), (Alphabet([1, 0]), [1], [0], [2])], -) -def test_rev_lenlex_cmp_object_with_alphabet(alphabet, x, y, missing): - """Check that the comparison object stores and uses its alphabet.""" - compare = RevLenLexCmp(alphabet=alphabet) - assert compare(x, y) - assert compare.alphabet() == alphabet - assert compare.init(alphabet) is compare +############################################################################### +# RevLenLex order +############################################################################### - with pytest.raises(LibsemigroupsError): - compare(x, missing) +def test_rev_lenlex_without_alphabet(): + """Check reverse length-lexicographic comparison without an alphabet.""" + assert rev_lenlex_cmp("ba", "ab") + assert rev_lenlex_cmp([1, 0], [0, 1]) -def test_rev_lenlex_cmp_object_rejects_bad_constructors(): - """Check that only zero arguments or one Alphabet are accepted.""" - with pytest.raises(TypeError): - RevLenLexCmp("abc") - with pytest.raises(TypeError): - RevLenLexCmp(Alphabet("abc"), Alphabet("abc")) + cmp = RevLenLexCmp() + assert cmp("a", "b") + assert cmp([0], [1]) + assert cmp("ba", "ab") -def test_rpo_cmp_object_without_alphabet(): - """Check the stateless comparison object with both word types.""" - compare = RPOCmp() - assert compare("a", "b") - assert compare([0], [1]) - assert not compare("ab", "ba") +def test_rev_lenlex_with_alphabet(): + """Check reverse length-lexicographic comparison with an alphabet.""" + alphabet = Alphabet("mkn") + x = "nm" + y = "nk" + assert rev_lenlex_cmp(alphabet, x, y) + cmp = RevLenLexCmp(alphabet=alphabet) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.init(alphabet) is cmp -@pytest.mark.parametrize( - ("alphabet", "x", "y", "missing"), - [(Alphabet("ba"), "b", "a", "c"), (Alphabet([1, 0]), [1], [0], [2])], -) -def test_rpo_cmp_object_with_alphabet(alphabet, x, y, missing): - """Check that the comparison object stores and uses its alphabet.""" - compare = RPOCmp(alphabet=alphabet) - assert compare(x, y) - assert compare.alphabet() == alphabet - assert compare.init(alphabet) is compare + with pytest.raises(LibsemigroupsError): + rev_lenlex_cmp(alphabet, x, "p") + with pytest.raises(LibsemigroupsError): + cmp(x, "p") + + alphabet = Alphabet([7, 4, 8]) + x = [8, 7] + y = [8, 4] + assert rev_lenlex_cmp(alphabet, x, y) + + cmp = RevLenLexCmp(alphabet) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.init(alphabet) is cmp with pytest.raises(LibsemigroupsError): - compare(x, missing) + rev_lenlex_cmp(alphabet, x, [3]) + with pytest.raises(LibsemigroupsError): + cmp(x, [3]) -def test_rpo_cmp_object_rejects_bad_constructors(): - """Check that only zero arguments or one Alphabet are accepted.""" - with pytest.raises(TypeError): - RPOCmp("abc") - with pytest.raises(TypeError): - RPOCmp(Alphabet("abc"), Alphabet("abc")) +def test_rev_lenlex_object_copy_and_repr(): + """Check copies and human-readable representations.""" + _check_simple_copy_and_repr(RevLenLexCmp, "RevLenLexCmp") -def test_rev_rpo_cmp_object_without_alphabet(): - """Check the stateless comparison object with both word types.""" - compare = RevRPOCmp() - assert compare("a", "b") - assert compare([0], [1]) - assert not compare("ba", "ab") +def test_rev_lenlex_object_rejects_bad_constructor_arguments(): + """Check constructor validation.""" + _check_simple_bad_constructor_arguments(RevLenLexCmp) -@pytest.mark.parametrize( - ("alphabet", "x", "y", "missing"), - [(Alphabet("ba"), "b", "a", "c"), (Alphabet([1, 0]), [1], [0], [2])], -) -def test_rev_rpo_cmp_object_with_alphabet(alphabet, x, y, missing): - """Check that the comparison object stores and uses its alphabet.""" - compare = RevRPOCmp(alphabet=alphabet) - assert compare(x, y) - assert compare.alphabet() == alphabet - assert compare.init(alphabet) is compare +############################################################################### +# RPO order +############################################################################### - with pytest.raises(LibsemigroupsError): - compare(x, missing) +def test_rpo_without_alphabet(): + """Check recursive-path comparison without an alphabet.""" + assert rpo_cmp("a", "b") + assert rpo_cmp([0], [1]) -def test_rev_rpo_cmp_object_rejects_bad_constructors(): - """Check that only zero arguments or one Alphabet are accepted.""" - with pytest.raises(TypeError): - RevRPOCmp("abc") - with pytest.raises(TypeError): - RevRPOCmp(Alphabet("abc"), Alphabet("abc")) + cmp = RPOCmp() + assert cmp("a", "b") + assert cmp([0], [1]) + assert not cmp("ab", "ba") -def test_wr_cmp_object_without_alphabet(): - """Check wreath-product comparison of list[int].""" - compare = WrCmp([1, 2]) - assert compare.levels() == [1, 2] - assert compare([0], [1]) - assert compare.init([2, 1]) is compare - assert compare.levels() == [2, 1] +def test_rpo_with_alphabet(): + """Check recursive-path comparison with an alphabet.""" + alphabet = Alphabet("zxy") + x = "z" + y = "x" + assert rpo_cmp(alphabet, x, y) + cmp = RPOCmp(alphabet=alphabet) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.init(alphabet) is cmp -@pytest.mark.parametrize( - ("alphabet", "x", "y", "missing"), - [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], -) -def test_wr_cmp_object_with_alphabet(alphabet, x, y, missing): - """Check alphabet-aware wreath-product comparison.""" - compare = WrCmp(alphabet, [1, 2]) - assert compare(x, y) - assert compare.alphabet() == alphabet - assert compare.levels() == [1, 2] - assert compare.init(alphabet, [1, 2]) is compare + with pytest.raises(LibsemigroupsError): + rpo_cmp(alphabet, x, "w") + with pytest.raises(LibsemigroupsError): + cmp(x, "w") + alphabet = Alphabet([8, 3, 5]) + x = [8] + y = [3] + assert rpo_cmp(alphabet, x, y) + + cmp = RPOCmp(alphabet) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.init(alphabet) is cmp + + with pytest.raises(LibsemigroupsError): + rpo_cmp(alphabet, x, [1]) with pytest.raises(LibsemigroupsError): - compare(x, missing) + cmp(x, [1]) -def test_wr_cmp_object_copy_and_repr(): +def test_rpo_object_copy_and_repr(): """Check copies and human-readable representations.""" - original = WrCmp(Alphabet("ab"), [1, 2]) - copies = ( - original.copy(), - original.__copy__(), # pylint: disable=unnecessary-dunder-call - copy(original), - ) - original.init(Alphabet("ab"), [2, 1]) - assert all(copied.levels() == [1, 2] for copied in copies) - assert repr(WrCmp([1, 2])) == "" - assert repr(WrCmp(Alphabet("ab"), [1, 2])) == ( - ' with levels [1, 2]>' - ) + _check_simple_copy_and_repr(RPOCmp, "RPOCmp") -def test_wr_cmp_object_rejects_bad_constructors(): +def test_rpo_object_rejects_bad_constructor_arguments(): """Check constructor validation.""" - with pytest.raises(TypeError): - WrCmp(Alphabet("ab")) - with pytest.raises(LibsemigroupsError): - WrCmp(Alphabet("ab"), [1]) + _check_simple_bad_constructor_arguments(RPOCmp) -def test_wt_lenlex_cmp_object_without_alphabet(): - """Check weighted lenlex comparison of list[int].""" - compare = WtLenLexCmp([1, 2]) - assert compare.weights() == [1, 2] - assert compare([0], [1]) - assert compare.init([2, 1]) is compare - assert compare.weights() == [2, 1] +############################################################################### +# RevRPO order +############################################################################### -@pytest.mark.parametrize( - ("alphabet", "x", "y", "missing"), - [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], -) -def test_wt_lenlex_cmp_object_with_alphabet(alphabet, x, y, missing): - """Check alphabet-aware weighted lenlex comparison.""" - compare = WtLenLexCmp(alphabet, [1, 2]) - assert compare(x, y) - assert compare.alphabet() == alphabet - assert compare.weights() == [1, 2] - assert compare.init(alphabet, [1, 2]) is compare +def test_rev_rpo_without_alphabet(): + """Check reverse recursive-path comparison without an alphabet.""" + assert rev_rpo_cmp("a", "b") + assert rev_rpo_cmp([0], [1]) + + cmp = RevRPOCmp() + assert cmp("a", "b") + assert cmp([0], [1]) + assert not cmp("ba", "ab") + +def test_rev_rpo_with_alphabet(): + """Check reverse recursive-path comparison with an alphabet.""" + alphabet = Alphabet("qsr") + x = "q" + y = "s" + assert rev_rpo_cmp(alphabet, x, y) + + cmp = RevRPOCmp(alphabet=alphabet) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.init(alphabet) is cmp + + with pytest.raises(LibsemigroupsError): + rev_rpo_cmp(alphabet, x, "t") with pytest.raises(LibsemigroupsError): - compare(x, missing) + cmp(x, "t") + alphabet = Alphabet([6, 4, 9]) + x = [6] + y = [4] + assert rev_rpo_cmp(alphabet, x, y) + + cmp = RevRPOCmp(alphabet) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.init(alphabet) is cmp + + with pytest.raises(LibsemigroupsError): + rev_rpo_cmp(alphabet, x, [2]) + with pytest.raises(LibsemigroupsError): + cmp(x, [2]) -def test_wt_lenlex_cmp_object_copy_and_repr(): + +def test_rev_rpo_object_copy_and_repr(): """Check copies and human-readable representations.""" - original = WtLenLexCmp(Alphabet("ab"), [1, 2]) - copies = ( - original.copy(), - original.__copy__(), # pylint: disable=unnecessary-dunder-call - copy(original), - ) - original.init(Alphabet("ab"), [2, 1]) - assert all(copied.weights() == [1, 2] for copied in copies) - assert repr(WtLenLexCmp([1, 2])) == "" - assert repr(WtLenLexCmp(Alphabet("ab"), [1, 2])) == ( - ' with weights [1, 2]>' - ) + _check_simple_copy_and_repr(RevRPOCmp, "RevRPOCmp") -def test_wt_lenlex_cmp_object_rejects_bad_constructors(): +def test_rev_rpo_object_rejects_bad_constructor_arguments(): """Check constructor validation.""" - with pytest.raises(TypeError): - WtLenLexCmp(Alphabet("ab")) + _check_simple_bad_constructor_arguments(RevRPOCmp) + + +############################################################################### +# Wr order +############################################################################### + + +def test_wr_without_alphabet(): + """Check wreath-product comparison without an alphabet.""" + levels = [0, 0, 1] + x = [0, 2] + y = [1, 2] + assert wr_cmp(levels, x, y) + assert not wr_cmp(levels, y, x) with pytest.raises(LibsemigroupsError): - WtLenLexCmp(Alphabet("ab"), [1]) + wr_cmp([0, 1], [0, 2], [0, 1]) + cmp = WrCmp(levels) + assert cmp(x, y) + assert cmp.levels() == levels + assert cmp.init([1, 1, 0]) is cmp + assert cmp.levels() == [1, 1, 0] + with pytest.raises(LibsemigroupsError): + cmp([3], [0]) -def test_rev_wt_lenlex_cmp_object_without_alphabet(): - """Check reversed weighted lenlex comparison of list[int].""" - compare = RevWtLenLexCmp([1, 2]) - assert compare.weights() == [1, 2] - assert compare([0], [1]) - assert compare.init([2, 1]) is compare - assert compare.weights() == [2, 1] +def test_wr_with_alphabet(): + """Check wreath-product comparison with an alphabet.""" + alphabet = Alphabet("bac") + levels = [1, 1, 0] + x = "cbcc" + y = "ccbc" + assert wr_cmp(alphabet, levels, x, y) + assert wr_cmp(alphabet, levels, "ac", "ca") -@pytest.mark.parametrize( - ("alphabet", "x", "y", "missing"), - [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], -) -def test_rev_wt_lenlex_cmp_object_with_alphabet(alphabet, x, y, missing): - """Check alphabet-aware reversed weighted lenlex comparison.""" - compare = RevWtLenLexCmp(alphabet, [1, 2]) - assert compare(x, y) - assert compare.alphabet() == alphabet - assert compare.weights() == [1, 2] - assert compare.init(alphabet, [1, 2]) is compare + cmp = WrCmp(alphabet, levels) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.levels() == levels + assert cmp.init(alphabet, levels) is cmp + + with pytest.raises(LibsemigroupsError): + wr_cmp(alphabet, levels, "d", "b") + with pytest.raises(LibsemigroupsError): + cmp("d", "b") + + alphabet = Alphabet([9, 4, 7]) + levels = [0, 0, 0] + x = [9] + y = [4] + assert wr_cmp(alphabet, levels, x, y) + + cmp = WrCmp(alphabet, levels) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.levels() == levels + assert cmp.init(alphabet, levels) is cmp with pytest.raises(LibsemigroupsError): - compare(x, missing) + wr_cmp(alphabet, levels, x, [2]) + with pytest.raises(LibsemigroupsError): + cmp(x, [2]) -def test_rev_wt_lenlex_cmp_object_copy_and_repr(): +def test_wr_object_copy_and_repr(): """Check copies and human-readable representations.""" - original = RevWtLenLexCmp(Alphabet("ab"), [1, 2]) - copies = ( - original.copy(), - original.__copy__(), # pylint: disable=unnecessary-dunder-call - copy(original), - ) - original.init(Alphabet("ab"), [2, 1]) - assert all(copied.weights() == [1, 2] for copied in copies) - assert repr(RevWtLenLexCmp([1, 2])) == "" - assert repr(RevWtLenLexCmp(Alphabet("ab"), [1, 2])) == ( - ' with weights [1, 2]>' - ) + _check_configured_copy_and_repr(WrCmp, "WrCmp", "levels", "levels") -def test_rev_wt_lenlex_cmp_object_rejects_bad_constructors(): +def test_wr_object_rejects_bad_constructor_arguments(): """Check constructor validation.""" - with pytest.raises(TypeError): - RevWtLenLexCmp(Alphabet("ab")) + _check_configured_bad_constructor_arguments(WrCmp) + + +############################################################################### +# RevWr order +############################################################################### + + +def test_rev_wr_without_alphabet(): + """Check reverse wreath-product comparison without an alphabet.""" + levels = [0, 0] + x = [1, 0] + y = [0, 1] + assert rev_wr_cmp(levels, x, y) + + cmp = RevWrCmp(levels) + assert cmp(x, y) + assert cmp.levels() == levels + assert cmp.init([2, 1]) is cmp + assert cmp.levels() == [2, 1] + + with pytest.raises(LibsemigroupsError): + rev_wr_cmp(levels, [2], [0]) with pytest.raises(LibsemigroupsError): - RevWtLenLexCmp(Alphabet("ab"), [1]) + cmp([2], [0]) -def test_wt_lex_cmp_object_without_alphabet(): - """Check weighted lexicographic comparison of list[int].""" - compare = WtLexCmp([1, 2]) - assert compare.weights() == [1, 2] - assert compare([0], [1]) - assert compare.init([2, 1]) is compare - assert compare.weights() == [2, 1] +def test_rev_wr_with_alphabet(): + """Check reverse wreath-product comparison with an alphabet.""" + alphabet = Alphabet("cab") + levels = [0, 0, 0] + x = "bc" + y = "ba" + assert rev_wr_cmp(alphabet, levels, x, y) + cmp = RevWrCmp(alphabet, levels) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.levels() == levels + assert cmp.init(alphabet, levels) is cmp -@pytest.mark.parametrize( - ("alphabet", "x", "y", "missing"), - [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], -) -def test_wt_lex_cmp_object_with_alphabet(alphabet, x, y, missing): - """Check alphabet-aware weighted lexicographic comparison.""" - compare = WtLexCmp(alphabet, [1, 2]) - assert compare(x, y) - assert compare.alphabet() == alphabet - assert compare.weights() == [1, 2] - assert compare.init(alphabet, [1, 2]) is compare + with pytest.raises(LibsemigroupsError): + rev_wr_cmp(alphabet, levels, x, "d") + with pytest.raises(LibsemigroupsError): + cmp(x, "d") + + alphabet = Alphabet([7, 2, 5]) + levels = [0, 0, 0] + x = [5, 7] + y = [5, 2] + assert rev_wr_cmp(alphabet, levels, x, y) + cmp = RevWrCmp(alphabet, levels) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.levels() == levels + assert cmp.init(alphabet, levels) is cmp + + with pytest.raises(LibsemigroupsError): + rev_wr_cmp(alphabet, levels, x, [9]) with pytest.raises(LibsemigroupsError): - compare(x, missing) + cmp(x, [9]) -def test_wt_lex_cmp_object_copy_and_repr(): +def test_rev_wr_object_copy_and_repr(): """Check copies and human-readable representations.""" - original = WtLexCmp(Alphabet("ab"), [1, 2]) - copies = ( - original.copy(), - original.__copy__(), # pylint: disable=unnecessary-dunder-call - copy(original), - ) - original.init(Alphabet("ab"), [2, 1]) - assert all(copied.weights() == [1, 2] for copied in copies) - assert repr(WtLexCmp([1, 2])) == "" - assert repr(WtLexCmp(Alphabet("ab"), [1, 2])) == ( - ' with weights [1, 2]>' - ) + _check_configured_copy_and_repr(RevWrCmp, "RevWrCmp", "levels", "levels") -def test_wt_lex_cmp_object_rejects_bad_constructors(): +def test_rev_wr_object_rejects_bad_constructor_arguments(): """Check constructor validation.""" - with pytest.raises(TypeError): - WtLexCmp(Alphabet("ab")) + _check_configured_bad_constructor_arguments(RevWrCmp) + + +############################################################################### +# WtLenLex order +############################################################################### + + +def test_wt_lenlex_without_alphabet(): + """Check weight-length-lexicographic comparison without an alphabet.""" + weights = [2, 1, 6] + x = [0, 1] + y = [2] + assert wt_lenlex_cmp(weights, x, y) + assert not wt_lenlex_cmp(weights, y, x) + assert wt_lenlex_cmp(weights, "\x00\x01", "\x02") + assert not wt_lenlex_cmp(weights, "\x02", "\x00\x01") + + cmp = WtLenLexCmp(weights) + assert cmp(x, y) + assert cmp.weights() == weights + assert cmp.init([1, 2, 6]) is cmp + assert cmp.weights() == [1, 2, 6] + + with pytest.raises(LibsemigroupsError): + wt_lenlex_cmp(weights, [3], [0]) + with pytest.raises(LibsemigroupsError): + wt_lenlex_cmp(weights, "\x00", "\x03") with pytest.raises(LibsemigroupsError): - WtLexCmp(Alphabet("ab"), [1]) + cmp([3], [0]) -def test_len_wt_lex_cmp_object_without_alphabet(): - """Check length then weighted lexicographic comparison of list[int].""" - compare = LenWtLexCmp([1, 2]) - assert compare.weights() == [1, 2] - assert compare([0], [1]) - assert compare.init([2, 1]) is compare - assert compare.weights() == [2, 1] +def test_wt_lenlex_with_alphabet(): + """Check weight-length-lexicographic comparison with an alphabet.""" + alphabet = Alphabet("cab") + weights = [4, 1, 3] + x = "aa" + y = "b" + assert wt_lenlex_cmp(alphabet, weights, x, y) + cmp = WtLenLexCmp(alphabet, weights) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.weights() == weights + assert cmp.init(alphabet, weights) is cmp -@pytest.mark.parametrize( - ("alphabet", "x", "y", "missing"), - [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], -) -def test_len_wt_lex_cmp_object_with_alphabet(alphabet, x, y, missing): - """Check alphabet-aware length then weighted lexicographic comparison.""" - compare = LenWtLexCmp(alphabet, [1, 2]) - assert compare(x, y) - assert compare.alphabet() == alphabet - assert compare.weights() == [1, 2] - assert compare.init(alphabet, [1, 2]) is compare + with pytest.raises(LibsemigroupsError): + wt_lenlex_cmp(alphabet, weights, x, "d") + with pytest.raises(LibsemigroupsError): + wt_lenlex_cmp(alphabet, [1, 2], x, y) + with pytest.raises(LibsemigroupsError): + cmp(x, "d") + + alphabet = Alphabet([7, 2, 5]) + weights = [4, 1, 3] + x = [2, 2] + y = [5] + assert wt_lenlex_cmp(alphabet, weights, x, y) + + cmp = WtLenLexCmp(alphabet, weights) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.weights() == weights + assert cmp.init(alphabet, weights) is cmp with pytest.raises(LibsemigroupsError): - compare(x, missing) + wt_lenlex_cmp(alphabet, weights, x, [9]) + with pytest.raises(LibsemigroupsError): + cmp(x, [9]) -def test_len_wt_lex_cmp_object_copy_and_repr(): +def test_wt_lenlex_object_copy_and_repr(): """Check copies and human-readable representations.""" - original = LenWtLexCmp(Alphabet("ab"), [1, 2]) - copies = ( - original.copy(), - original.__copy__(), # pylint: disable=unnecessary-dunder-call - copy(original), - ) - original.init(Alphabet("ab"), [2, 1]) - assert all(copied.weights() == [1, 2] for copied in copies) - assert repr(LenWtLexCmp([1, 2])) == "" - assert repr(LenWtLexCmp(Alphabet("ab"), [1, 2])) == ( - ' with weights [1, 2]>' - ) + _check_configured_copy_and_repr(WtLenLexCmp, "WtLenLexCmp", "weights", "weights") -def test_len_wt_lex_cmp_object_rejects_bad_constructors(): +def test_wt_lenlex_object_rejects_bad_constructor_arguments(): """Check constructor validation.""" - with pytest.raises(TypeError): - LenWtLexCmp(Alphabet("ab")) + _check_configured_bad_constructor_arguments(WtLenLexCmp) + + +############################################################################### +# RevWtLenLex order +############################################################################### + + +def test_rev_wt_lenlex_without_alphabet(): + """Check reverse weight-length-lexicographic comparison without an alphabet.""" + weights = [1, 1] + x = [1, 0] + y = [0, 1] + assert rev_wt_lenlex_cmp(weights, x, y) + + cmp = RevWtLenLexCmp(weights) + assert cmp(x, y) + assert cmp.weights() == weights + assert cmp.init([2, 1]) is cmp + assert cmp.weights() == [2, 1] + + assert rev_wt_lenlex_cmp([1, 2], [1], [0, 0]) with pytest.raises(LibsemigroupsError): - LenWtLexCmp(Alphabet("ab"), [1]) + rev_wt_lenlex_cmp(weights, [2], [0]) + with pytest.raises(LibsemigroupsError): + cmp([2], [0]) -def test_rev_len_wt_lex_cmp_object_without_alphabet(): - """Check length then reversed weighted lexicographic comparison of list[int].""" - compare = RevLenWtLexCmp([1, 2]) - assert compare.weights() == [1, 2] - assert compare([0], [1]) - assert compare.init([2, 1]) is compare - assert compare.weights() == [2, 1] +def test_rev_wt_lenlex_with_alphabet(): + """Check reverse weight-length-lexicographic comparison with an alphabet.""" + alphabet = Alphabet("pqr") + weights = [1, 1, 1] + x = "rp" + y = "rq" + assert rev_wt_lenlex_cmp(alphabet, weights, x, y) + cmp = RevWtLenLexCmp(alphabet, weights) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.weights() == weights + assert cmp.init(alphabet, weights) is cmp -@pytest.mark.parametrize( - ("alphabet", "x", "y", "missing"), - [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], -) -def test_rev_len_wt_lex_cmp_object_with_alphabet(alphabet, x, y, missing): - """Check alphabet-aware length then reversed weighted lexicographic comparison.""" - compare = RevLenWtLexCmp(alphabet, [1, 2]) - assert compare(x, y) - assert compare.alphabet() == alphabet - assert compare.weights() == [1, 2] - assert compare.init(alphabet, [1, 2]) is compare + with pytest.raises(LibsemigroupsError): + rev_wt_lenlex_cmp(alphabet, weights, x, "s") + with pytest.raises(LibsemigroupsError): + cmp(x, "s") + + alphabet = Alphabet([6, 1, 8]) + weights = [1, 1, 1] + x = [8, 6] + y = [8, 1] + assert rev_wt_lenlex_cmp(alphabet, weights, x, y) + + cmp = RevWtLenLexCmp(alphabet, weights) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.weights() == weights + assert cmp.init(alphabet, weights) is cmp with pytest.raises(LibsemigroupsError): - compare(x, missing) + rev_wt_lenlex_cmp(alphabet, weights, x, [4]) + with pytest.raises(LibsemigroupsError): + cmp(x, [4]) -def test_rev_len_wt_lex_cmp_object_copy_and_repr(): +def test_rev_wt_lenlex_object_copy_and_repr(): """Check copies and human-readable representations.""" - original = RevLenWtLexCmp(Alphabet("ab"), [1, 2]) - copies = ( - original.copy(), - original.__copy__(), # pylint: disable=unnecessary-dunder-call - copy(original), - ) - original.init(Alphabet("ab"), [2, 1]) - assert all(copied.weights() == [1, 2] for copied in copies) - assert repr(RevLenWtLexCmp([1, 2])) == "" - assert repr(RevLenWtLexCmp(Alphabet("ab"), [1, 2])) == ( - ' with weights [1, 2]>' + _check_configured_copy_and_repr( + RevWtLenLexCmp, "RevWtLenLexCmp", "weights", "weights" ) -def test_rev_len_wt_lex_cmp_object_rejects_bad_constructors(): +def test_rev_wt_lenlex_object_rejects_bad_constructor_arguments(): """Check constructor validation.""" - with pytest.raises(TypeError): - RevLenWtLexCmp(Alphabet("ab")) + _check_configured_bad_constructor_arguments(RevWtLenLexCmp) + + +############################################################################### +# WtLex order +############################################################################### + + +def test_wt_lex_without_alphabet(): + """Check weight-lexicographic comparison without an alphabet.""" + weights = [2, 1, 6] + x = [0, 1] + y = [2] + assert wt_lex_cmp(weights, x, y) + assert not wt_lex_cmp(weights, y, x) + assert wt_lex_cmp(weights, "\x00\x01", "\x02") + assert not wt_lex_cmp(weights, "\x02", "\x00\x01") + + cmp = WtLexCmp(weights) + assert cmp(x, y) + assert cmp.weights() == weights + assert cmp.init([1, 2, 6]) is cmp + assert cmp.weights() == [1, 2, 6] + + assert not wt_lex_cmp([1, 2], [1], [0, 0]) with pytest.raises(LibsemigroupsError): - RevLenWtLexCmp(Alphabet("ab"), [1]) + wt_lex_cmp(weights, [3], [0]) + with pytest.raises(LibsemigroupsError): + wt_lex_cmp(weights, "\x00", "\x03") + with pytest.raises(LibsemigroupsError): + cmp([3], [0]) -def test_rev_wt_lex_cmp_object_without_alphabet(): - """Check reversed weighted lexicographic comparison of list[int].""" - compare = RevWtLexCmp([1, 2]) - assert compare.weights() == [1, 2] - assert compare([0], [1]) - assert compare.init([2, 1]) is compare - assert compare.weights() == [2, 1] +def test_wt_lex_with_alphabet(): + """Check weight-lexicographic comparison with an alphabet.""" + alphabet = Alphabet("dbca") + weights = [5, 2, 3, 1] + x = "b" + y = "aa" + assert wt_lex_cmp(alphabet, weights, x, y) + cmp = WtLexCmp(alphabet, weights) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.weights() == weights + assert cmp.init(alphabet, weights) is cmp -@pytest.mark.parametrize( - ("alphabet", "x", "y", "missing"), - [(Alphabet("ab"), "a", "b", "c"), (Alphabet([0, 1]), [0], [1], [2])], -) -def test_rev_wt_lex_cmp_object_with_alphabet(alphabet, x, y, missing): - """Check alphabet-aware reversed weighted lexicographic comparison.""" - compare = RevWtLexCmp(alphabet, [1, 2]) - assert compare(x, y) - assert compare.alphabet() == alphabet - assert compare.weights() == [1, 2] - assert compare.init(alphabet, [1, 2]) is compare + with pytest.raises(LibsemigroupsError): + wt_lex_cmp(alphabet, weights, x, "e") + with pytest.raises(LibsemigroupsError): + wt_lex_cmp(alphabet, [1, 2, 3], x, y) + with pytest.raises(LibsemigroupsError): + cmp(x, "e") + + alphabet = Alphabet([9, 4, 7, 2]) + weights = [5, 2, 3, 1] + x = [4] + y = [2, 2] + assert wt_lex_cmp(alphabet, weights, x, y) + cmp = WtLexCmp(alphabet, weights) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.weights() == weights + assert cmp.init(alphabet, weights) is cmp + + with pytest.raises(LibsemigroupsError): + wt_lex_cmp(alphabet, weights, x, [6]) with pytest.raises(LibsemigroupsError): - compare(x, missing) + cmp(x, [6]) -def test_rev_wt_lex_cmp_object_copy_and_repr(): +def test_wt_lex_object_copy_and_repr(): """Check copies and human-readable representations.""" - original = RevWtLexCmp(Alphabet("ab"), [1, 2]) - copies = ( - original.copy(), - original.__copy__(), # pylint: disable=unnecessary-dunder-call - copy(original), - ) - original.init(Alphabet("ab"), [2, 1]) - assert all(copied.weights() == [1, 2] for copied in copies) - assert repr(RevWtLexCmp([1, 2])) == "" - assert repr(RevWtLexCmp(Alphabet("ab"), [1, 2])) == ( - ' with weights [1, 2]>' - ) + _check_configured_copy_and_repr(WtLexCmp, "WtLexCmp", "weights", "weights") -def test_rev_wt_lex_cmp_object_rejects_bad_constructors(): +def test_wt_lex_object_rejects_bad_constructor_arguments(): """Check constructor validation.""" - with pytest.raises(TypeError): - RevWtLexCmp(Alphabet("ab")) - with pytest.raises(LibsemigroupsError): - RevWtLexCmp(Alphabet("ab"), [1]) - - -@pytest.mark.parametrize( - "comparison_type", - [ - LexCmp, - RevLexCmp, - LenLexCmp, - RevLenLexCmp, - RPOCmp, - RevRPOCmp, - WrCmp, - RevWrCmp, - WtLenLexCmp, - RevWtLenLexCmp, - WtLexCmp, - RevWtLexCmp, - LenWtLexCmp, - RevLenWtLexCmp, - ], -) -def test_comparison_object_init_requires_an_argument(comparison_type): - """Check comparison objects have no zero-argument init overload.""" - with pytest.raises((AttributeError, TypeError)): - comparison_type().init() + _check_configured_bad_constructor_arguments(WtLexCmp) -@pytest.mark.parametrize( - "comparison_type", [LexCmp, RevLexCmp, LenLexCmp, RevLenLexCmp, RPOCmp, RevRPOCmp] -) -@pytest.mark.parametrize( - ("alphabet", "replacement", "x", "y"), - [ - (None, None, "a", "b"), - (Alphabet("ba"), Alphabet("ab"), "b", "a"), - (Alphabet([1, 0]), Alphabet([0, 1]), [1], [0]), - ], -) -def test_comparison_object_copy(comparison_type, alphabet, replacement, x, y): - """Check that both copy APIs make independent copies of the C++ object.""" - original = comparison_type() if alphabet is None else comparison_type(alphabet) - copies = ( - original.copy(), - original.__copy__(), # pylint: disable=unnecessary-dunder-call - copy(original), - ) +############################################################################### +# RevWtLex order +############################################################################### - for copied in copies: - assert isinstance(copied, comparison_type) - assert copied is not original - assert copied.py_template_params == original.py_template_params - assert copied(x, y) - if alphabet is not None: - assert copied.alphabet() == alphabet - if replacement is not None: - original.init(replacement) - assert not original(x, y) - assert all(copied(x, y) for copied in copies) +def test_rev_wt_lex_without_alphabet(): + """Check reverse weight-lexicographic comparison without an alphabet.""" + weights = [1, 1] + x = [1, 0] + y = [0, 1] + assert rev_wt_lex_cmp(weights, x, y) + + cmp = RevWtLexCmp(weights) + assert cmp(x, y) + assert cmp.weights() == weights + assert cmp.init([2, 1]) is cmp + assert cmp.weights() == [2, 1] + + assert not rev_wt_lex_cmp([1, 2], [1], [0, 0]) + assert not rev_wt_lex_cmp([100, 1], [0], [1, 1]) + with pytest.raises(LibsemigroupsError): + rev_wt_lex_cmp(weights, [2], [0]) + with pytest.raises(LibsemigroupsError): + cmp([2], [0]) -@pytest.mark.parametrize( - ("comparison_type", "name"), - [ - (LexCmp, "LexCmp"), - (RevLexCmp, "RevLexCmp"), - (LenLexCmp, "LenLexCmp"), - (RevLenLexCmp, "RevLenLexCmp"), - (RPOCmp, "RPOCmp"), - (RevRPOCmp, "RevRPOCmp"), - ], -) -def test_comparison_object_repr(comparison_type, name): - """Check human-readable representations with and without alphabets.""" - assert repr(comparison_type()) == f"<{name} object>" - assert repr(comparison_type(Alphabet("ba"))) == f'<{name} object over >' - assert repr(comparison_type(Alphabet([1, 0]))) == f"<{name} object over >" - - -@pytest.mark.parametrize( - ("compare", "x", "y"), - [ - (lex_cmp, "ab", "ba"), - (lex_cmp, [0, 1], [1, 0]), - (lenlex_cmp, "ba", "aaa"), - (lenlex_cmp, [1, 0], [0, 0, 0]), - (rev_lex_cmp, "ab", "ba"), - (rev_lex_cmp, [0, 1], [1, 0]), - (rev_lenlex_cmp, "ba", "aaa"), - (rev_lenlex_cmp, [1, 0], [0, 0, 0]), - (rpo_cmp, "ab", "ba"), - (rpo_cmp, [0, 1], [1, 0]), - (rev_rpo_cmp, "ab", "ba"), - (rev_rpo_cmp, [0, 1], [1, 0]), - ], -) -def test_compare_without_alphabet(compare, x, y): - """Check that each comparison function works without an alphabet.""" - assert isinstance(compare(x, y), bool) +def test_rev_wt_lex_with_alphabet(): + """Check reverse weight-lexicographic comparison with an alphabet.""" + alphabet = Alphabet("uvw") + weights = [1, 1, 1] + x = "wu" + y = "wv" + assert rev_wt_lex_cmp(alphabet, weights, x, y) + cmp = RevWtLexCmp(alphabet, weights) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.weights() == weights + assert cmp.init(alphabet, weights) is cmp -@pytest.mark.parametrize( - "compare", [lex_cmp, lenlex_cmp, rev_lex_cmp, rev_lenlex_cmp, rpo_cmp, rev_rpo_cmp] -) -@pytest.mark.parametrize( - ("alphabet", "x", "y", "missing"), - [(Alphabet("ba"), "ba", "ab", "c"), (Alphabet([1, 0]), [1, 0], [0, 1], [2])], -) -def test_compare_with_alphabet(compare, alphabet, x, y, missing): - """Check alphabet-aware comparisons and reject unknown letters.""" - assert isinstance(compare(alphabet, x, y), bool) - with pytest.raises(LibsemigroupsError): - compare(alphabet, x, x + missing) + rev_wt_lex_cmp(alphabet, weights, x, "x") + with pytest.raises(LibsemigroupsError): + cmp(x, "x") + alphabet = Alphabet([3, 8, 1]) + weights = [1, 1, 1] + x = [1, 3] + y = [1, 8] + assert rev_wt_lex_cmp(alphabet, weights, x, y) -def test_order_values_and_backwards_compatible_aliases(): - """Check the new order values and their backwards-compatible aliases.""" - assert Order.shortlex == Order.lenlex - assert Order.recursive == Order.rev_rpo - assert Order.rpo != Order.rev_rpo + cmp = RevWtLexCmp(alphabet, weights) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.weights() == weights + assert cmp.init(alphabet, weights) is cmp + with pytest.raises(LibsemigroupsError): + rev_wt_lex_cmp(alphabet, weights, x, [6]) + with pytest.raises(LibsemigroupsError): + cmp(x, [6]) -@pytest.mark.parametrize( - ("old_compare", "new_compare"), - [ - (lexicographical_compare, lex_cmp), - (shortlex_compare, lenlex_cmp), - (recursive_path_compare, rev_rpo_cmp), - ], -) -def test_deprecated_comparisons(old_compare, new_compare): - """Check that legacy comparisons warn and delegate to their replacements.""" - with pytest.deprecated_call(): - result = old_compare("ab", "ba") - assert result == new_compare("ab", "ba") +def test_rev_wt_lex_object_copy_and_repr(): + """Check copies and human-readable representations.""" + _check_configured_copy_and_repr(RevWtLexCmp, "RevWtLexCmp", "weights", "weights") -def test_wr_cmp_for_integer_words(): - """Check wreath-product comparison, validation, and its unchecked form.""" - levels = [0, 0, 1] - x = [0, 2] - y = [1, 2] - assert wr_cmp(levels, x, y) - assert not wr_cmp(levels, y, x) +def test_rev_wt_lex_object_rejects_bad_constructor_arguments(): + """Check constructor validation.""" + _check_configured_bad_constructor_arguments(RevWtLexCmp) - with pytest.raises(LibsemigroupsError): - wr_cmp([0, 1], [0, 2], [0, 1]) +############################################################################### +# LenWtLex order +############################################################################### -def test_wr_cmp_with_alphabet(): - """Check wreath-product comparison over an explicitly ordered alphabet.""" - alphabet = Alphabet("bac") - levels = [1, 1, 0] - assert wr_cmp(alphabet, levels, "cbcc", "ccbc") - assert wr_cmp(alphabet, levels, "ac", "ca") +def test_len_wt_lex_without_alphabet(): + """Check length-weight-lexicographic comparison without an alphabet.""" + weights = [1, 1] + x = [0, 1] + y = [1, 0] + assert len_wt_lex_cmp(weights, x, y) - word_alphabet = Alphabet([1, 0]) - assert wr_cmp(word_alphabet, [0, 0], [1], [0]) + cmp = LenWtLexCmp(weights) + assert cmp(x, y) + assert cmp.weights() == weights + assert cmp.init([2, 1]) is cmp + assert cmp.weights() == [2, 1] + assert len_wt_lex_cmp([100, 1], [0], [1, 1]) with pytest.raises(LibsemigroupsError): - wr_cmp(alphabet, levels, "d", "b") + len_wt_lex_cmp(weights, [2], [0]) + with pytest.raises(LibsemigroupsError): + cmp([2], [0]) -@pytest.mark.parametrize("compare", [wt_lenlex_cmp, wt_lex_cmp]) -def test_weighted_comparisons_for_integer_words(compare): - """Check weighted comparisons and validation for integer words.""" - weights = [2, 1, 6] +def test_len_wt_lex_with_alphabet(): + """Check length-weight-lexicographic comparison with an alphabet.""" + alphabet = Alphabet("acb") + weights = [2, 1, 3] + x = "cc" + y = "aa" + assert len_wt_lex_cmp(alphabet, weights, x, y) - assert compare(weights, [0, 1], [2]) - assert not compare(weights, [2], [0, 1]) + cmp = LenWtLexCmp(alphabet, weights) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.weights() == weights + assert cmp.init(alphabet, weights) is cmp with pytest.raises(LibsemigroupsError): - compare(weights, [0], [3]) + len_wt_lex_cmp(alphabet, weights, x, "d") + with pytest.raises(LibsemigroupsError): + cmp(x, "d") - assert compare(weights, chr(0) + chr(1), chr(2)) - assert not compare(weights, chr(2), chr(0) + chr(1)) + alphabet = Alphabet([5, 1, 8]) + weights = [2, 1, 3] + x = [1, 1] + y = [5, 5] + assert len_wt_lex_cmp(alphabet, weights, x, y) + cmp = LenWtLexCmp(alphabet, weights) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.weights() == weights + assert cmp.init(alphabet, weights) is cmp + + with pytest.raises(LibsemigroupsError): + len_wt_lex_cmp(alphabet, weights, x, [9]) with pytest.raises(LibsemigroupsError): - compare(weights, chr(0), chr(3)) + cmp(x, [9]) -def test_weighted_comparisons_use_different_tie_breakers(): - """Check the length and lexicographic tie breakers differ.""" - weights = [1, 2] +def test_len_wt_lex_object_copy_and_repr(): + """Check copies and human-readable representations.""" + _check_configured_copy_and_repr(LenWtLexCmp, "LenWtLexCmp", "weights", "weights") - assert wt_lenlex_cmp(weights, [1], [0, 0]) - assert not wt_lex_cmp(weights, [1], [0, 0]) +def test_len_wt_lex_object_rejects_bad_constructor_arguments(): + """Check constructor validation.""" + _check_configured_bad_constructor_arguments(LenWtLexCmp) -@pytest.mark.parametrize("compare", [wt_lenlex_cmp, wt_lex_cmp]) -def test_weighted_comparisons_with_alphabet(compare): - """Check weighted comparisons over an explicitly ordered alphabet.""" - alphabet = Alphabet("ba") - assert compare(alphabet, [10, 1], "a", "b") - assert compare(alphabet, [1, 1], "b", "a") +############################################################################### +# RevLenWtLex order +############################################################################### - word_alphabet = Alphabet([1, 0]) - assert compare(word_alphabet, [1, 1], [1], [0]) - with pytest.raises(LibsemigroupsError): - compare(alphabet, [1, 1], "c", "a") +def test_rev_len_wt_lex_without_alphabet(): + """Check reverse length-weight-lexicographic comparison without an alphabet.""" + weights = [1, 1] + x = [1, 0] + y = [0, 1] + assert rev_len_wt_lex_cmp(weights, x, y) + cmp = RevLenWtLexCmp(weights) + assert cmp(x, y) + assert cmp.weights() == weights + assert cmp.init([2, 1]) is cmp + assert cmp.weights() == [2, 1] + + assert rev_len_wt_lex_cmp([100, 1], [0], [1, 1]) + with pytest.raises(LibsemigroupsError): + rev_len_wt_lex_cmp(weights, [2], [0]) with pytest.raises(LibsemigroupsError): - compare(alphabet, [1], "a", "b") + cmp([2], [0]) -@pytest.mark.parametrize( - ("compare", "parameters"), - [ - (rev_wr_cmp, [0, 0]), - (rev_wt_lenlex_cmp, [1, 1]), - (rev_wt_lex_cmp, [1, 1]), - (rev_len_wt_lex_cmp, [1, 1]), - ], -) -def test_parameterized_reverse_comparisons(compare, parameters): - """Check parameterized reverse orders for both supported word types.""" - assert compare(parameters, [1, 0], [0, 1]) - assert compare(Alphabet("ab"), parameters, "ba", "ab") +def test_rev_len_wt_lex_with_alphabet(): + """Check reverse length-weight-lexicographic comparison with an alphabet.""" + alphabet = Alphabet("hgf") + weights = [1, 1, 1] + x = "fh" + y = "fg" + assert rev_len_wt_lex_cmp(alphabet, weights, x, y) + + cmp = RevLenWtLexCmp(alphabet, weights) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.weights() == weights + assert cmp.init(alphabet, weights) is cmp with pytest.raises(LibsemigroupsError): - compare(parameters, [2], [0, 1]) + rev_len_wt_lex_cmp(alphabet, weights, x, "i") + with pytest.raises(LibsemigroupsError): + cmp(x, "i") + + alphabet = Alphabet([4, 6, 2]) + weights = [1, 1, 1] + x = [2, 4] + y = [2, 6] + assert rev_len_wt_lex_cmp(alphabet, weights, x, y) + cmp = RevLenWtLexCmp(alphabet, weights) + assert cmp(x, y) + assert cmp.alphabet() == alphabet + assert cmp.weights() == weights + assert cmp.init(alphabet, weights) is cmp + + with pytest.raises(LibsemigroupsError): + rev_len_wt_lex_cmp(alphabet, weights, x, [7]) with pytest.raises(LibsemigroupsError): - compare(Alphabet("ab"), parameters, "c", "ab") + cmp(x, [7]) -def test_reverse_weighted_comparisons_use_the_expected_priorities(): - """Check the weight and length priorities of the reverse weighted orders.""" - weights = [1, 2] +def test_rev_len_wt_lex_object_copy_and_repr(): + """Check copies and human-readable representations.""" + _check_configured_copy_and_repr( + RevLenWtLexCmp, "RevLenWtLexCmp", "weights", "weights" + ) - assert rev_wt_lenlex_cmp(weights, [1], [0, 0]) - assert not rev_wt_lex_cmp(weights, [1], [0, 0]) - weights = [100, 1] - assert rev_len_wt_lex_cmp(weights, [0], [1, 1]) - assert not rev_wt_lex_cmp(weights, [0], [1, 1]) +def test_rev_len_wt_lex_object_rejects_bad_constructor_arguments(): + """Check constructor validation.""" + _check_configured_bad_constructor_arguments(RevLenWtLexCmp) -def test_len_wt_lex_cmp(): - """Check length-before-weighted-lex ordering and validation.""" - weights = [1, 1] +############################################################################### +# Backwards compatibility +############################################################################### - assert len_wt_lex_cmp(weights, [0, 1], [1, 0]) - assert len_wt_lex_cmp(Alphabet("ab"), weights, "ab", "ba") - with pytest.raises(LibsemigroupsError): - len_wt_lex_cmp(weights, [2], [0, 1]) +def test_order_values_and_backwards_compatible_aliases(): + """Check enum values and their backwards-compatible aliases.""" + assert Order.shortlex == Order.lenlex + assert Order.recursive == Order.rev_rpo + assert Order.rpo != Order.rev_rpo - with pytest.raises(LibsemigroupsError): - len_wt_lex_cmp(Alphabet("ab"), weights, "c", "ab") - weights = [100, 1] - assert len_wt_lex_cmp(weights, [0], [1, 1]) +def test_deprecated_comparisons(): + """Check that legacy comparisons warn and delegate to their replacements.""" + with pytest.deprecated_call(): + result = lexicographical_compare("ac", "ba") + assert result == lex_cmp("ac", "ba") + + with pytest.deprecated_call(): + result = shortlex_compare("cab", "dddd") + assert result == lenlex_cmp("cab", "dddd") + + with pytest.deprecated_call(): + result = recursive_path_compare("xy", "yx") + assert result == rev_rpo_cmp("xy", "yx") From 2dd6a3be45dcb6dc0eb32090a9ff5efdfad0df2f Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Thu, 20 Aug 2026 12:28:16 +0100 Subject: [PATCH 23/27] Apply suggestions from code review Co-authored-by: Joe Edwards <80713360+Joseph-Edwards@users.noreply.github.com> --- src/order.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/order.cpp b/src/order.cpp index b15db0e2..43a06279 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -2844,7 +2844,7 @@ respectively, in new code. lexicographically. .. py:attribute:: Order.shortlex - :value: + :value: The short-lex ordering. Words are first ordered by length, and then lexicographically. @@ -2852,27 +2852,27 @@ respectively, in new code. This is deprecated; use :any:`Order.lenlex` instead. .. py:attribute:: Order.lex - :value: + :value: The lexicographic ordering. Note that this is not a well-order, so there may not be a lexicographically least word in a given congruence class of words. .. py:attribute:: Order.rpo - :value: + :value: The recursive-path ordering, as described in :cite:`Jantzen2012aa` (Definition 1.2.14, page 24). .. py:attribute:: Order.rev_rpo - :value: + :value: The reversed recursive-path ordering, based on the description in :cite:`Jantzen2012aa` (Definition 1.2.14, page 24), where words are read right-to-left before ordering. .. py:attribute:: Order.recursive - :value: + :value: The recursive-path ordering, as described in :cite:`Jantzen2012aa` (Definition 1.2.14, page 24). From 70131252fb566324f1f0fc4201a460c3185ad196 Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Thu, 20 Aug 2026 09:37:01 +0100 Subject: [PATCH 24/27] Format + lint --- src/order.cpp | 2 +- tests/test_order.py | 13 +++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/order.cpp b/src/order.cpp index 43a06279..6e94e9fd 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -2815,7 +2815,7 @@ Return the alphabet used to compare letters. )pbdoc", py::return_value_policy::reference_internal); } // bind_rev_lenlex_cmp_with_alphabet - } // namespace + } // namespace void init_order(py::module& m) { py::options options; diff --git a/tests/test_order.py b/tests/test_order.py index 50910b87..f5ba3d17 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -62,10 +62,7 @@ def _check_simple_copy_and_repr(Cmp, name): ) assert all(type(copied) is Cmp for copied in copies) assert all(copied is not original for copied in copies) - assert all( - copied.py_template_params == original.py_template_params - for copied in copies - ) + assert all(copied.py_template_params == original.py_template_params for copied in copies) assert all(copied(x, y) for copied in copies) if alphabet is not None: assert all(copied.alphabet() == alphabet for copied in copies) @@ -768,9 +765,7 @@ def test_rev_wt_lenlex_with_alphabet(): def test_rev_wt_lenlex_object_copy_and_repr(): """Check copies and human-readable representations.""" - _check_configured_copy_and_repr( - RevWtLenLexCmp, "RevWtLenLexCmp", "weights", "weights" - ) + _check_configured_copy_and_repr(RevWtLenLexCmp, "RevWtLenLexCmp", "weights", "weights") def test_rev_wt_lenlex_object_rejects_bad_constructor_arguments(): @@ -1066,9 +1061,7 @@ def test_rev_len_wt_lex_with_alphabet(): def test_rev_len_wt_lex_object_copy_and_repr(): """Check copies and human-readable representations.""" - _check_configured_copy_and_repr( - RevLenWtLexCmp, "RevLenWtLexCmp", "weights", "weights" - ) + _check_configured_copy_and_repr(RevLenWtLexCmp, "RevLenWtLexCmp", "weights", "weights") def test_rev_len_wt_lex_object_rejects_bad_constructor_arguments(): From 6386af0cbba76cd5735408721bf49ccc058f81e1 Mon Sep 17 00:00:00 2001 From: Codex OpenAI Date: Thu, 20 Aug 2026 12:30:22 +0100 Subject: [PATCH 25/27] order: allow strings for all Default Cmp structs --- src/order.cpp | 45 +++++++++++++++++++++++++++++++++------------ tests/test_order.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/src/order.cpp b/src/order.cpp index 6e94e9fd..2f5ab3d8 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -1324,7 +1324,7 @@ Construct a comparison object with an empty {1} vector. py::arg(configuration.c_str()), fmt::format(R"pbdoc( :sig=(self: {0}, {1}: list[int]) -> None: -Construct a comparison object for ``list[int]`` words. +Construct a comparison object for ``str`` or ``list[int]`` words. :param {1}: the {1} of the generators. :type {1}: list[int] @@ -1379,6 +1379,13 @@ Reinitialize a {0} object from a {1} list. thing.def(configuration.c_str(), [get_configuration](Cmp const& self) { return (self.*get_configuration)(); }); + thing.def( + "__call__", + [](Cmp const& self, std::string const& x, std::string const& y) { + return self(x, y); + }, + py::arg("x"), + py::arg("y")); thing.def( "__call__", [](Cmp const& self, word_type const& x, word_type const& y) { @@ -1396,15 +1403,21 @@ Reinitialize a {0} object from a {1} list. std::string const& configuration, std::string const& ordering, std::vector const& (Cmp::*get_configuration)() const) { - py::class_ thing(m, + std::string const non_alphabet_string_doctest = fmt::format(R"pbdoc( + >>> {0}([1, 2])("\x00", "\x01") + True +)pbdoc", + name); + py::class_ thing(m, binding_name, fmt::format(R"pbdoc( Compare words using {2}. -Use ``{0}({1})`` to compare ``list[int]`` words, where the entries of -*{1}* correspond to the indices. Use ``{0}(alphabet, {1})`` to compare words -whose letters belong to *alphabet*. The latter form copies both arguments and -only accepts words with the same type as *alphabet*. +Use ``{0}({1})`` to compare either ``str`` or ``list[int]`` words. In this +form, every letter is interpreted as an index into *{1}*. Use +``{0}(alphabet, {1})`` to compare words whose letters belong to *alphabet*. +The latter form copies both arguments and only accepts words with the same +type as *alphabet*. .. note:: The constructor fixes whether this object is alphabet-aware and fixes the @@ -1423,10 +1436,12 @@ only accepts words with the same type as *alphabet*. True >>> {0}(Alphabet([0, 1]), [1, 2])([0], [1]) True +{3} )pbdoc", name, configuration, - ordering) + ordering, + non_alphabet_string_doctest) .c_str()); thing.def(py::init const&, std::vector const&>(), @@ -1534,8 +1549,9 @@ and the alphabet and {1} list must have the same size. :sig=(self: {0}, x: str | list[int], y: str | list[int]) -> bool: Compare two words using {2}. -Non-alphabet aware objects accept ``list[int]``. Alphabet-aware objects accept -words of the same type as their alphabet. +Non-alphabet-aware objects accept either ``str`` or ``list[int]`` words, and +every letter is interpreted as an index into the stored *{1}*. Alphabet-aware +objects accept words of the same type as their alphabet. :param x: the first word. :type x: str | list[int] @@ -1546,9 +1562,12 @@ words of the same type as their alphabet. :rtype: bool :raises TypeError: - if the word types do not match the type used when the object was constructed. + if *x* and *y* do not both have the supported word type, or if their type + does not match the alphabet used to construct an alphabet-aware object. :raises LibsemigroupsError: - if a word contains a letter not represented by the stored weights or levels. + if a non-alphabet-aware word contains a letter that is not a valid index into + the stored *{1}*, or an alphabet-aware word contains a letter outside its + alphabet. .. doctest:: python @@ -1557,10 +1576,12 @@ words of the same type as their alphabet. True >>> {0}(Alphabet("ab"), [1, 2])("a", "b") True +{3} )pbdoc", name, configuration, - ordering) + ordering, + non_alphabet_string_doctest) .c_str()); thing.def("alphabet", &Cmp::alphabet, diff --git a/tests/test_order.py b/tests/test_order.py index f5ba3d17..a70612ad 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -497,11 +497,16 @@ def test_wr_without_alphabet(): cmp = WrCmp(levels) assert cmp(x, y) + assert cmp("\x00\x02", "\x01\x02") assert cmp.levels() == levels assert cmp.init([1, 1, 0]) is cmp assert cmp.levels() == [1, 1, 0] with pytest.raises(LibsemigroupsError): cmp([3], [0]) + with pytest.raises(LibsemigroupsError): + cmp("\x03", "\x00") + with pytest.raises(TypeError): + cmp("\x00", [1]) def test_wr_with_alphabet(): @@ -566,6 +571,7 @@ def test_rev_wr_without_alphabet(): cmp = RevWrCmp(levels) assert cmp(x, y) + assert cmp("\x01\x00", "\x00\x01") assert cmp.levels() == levels assert cmp.init([2, 1]) is cmp assert cmp.levels() == [2, 1] @@ -574,6 +580,10 @@ def test_rev_wr_without_alphabet(): rev_wr_cmp(levels, [2], [0]) with pytest.raises(LibsemigroupsError): cmp([2], [0]) + with pytest.raises(LibsemigroupsError): + cmp("\x02", "\x00") + with pytest.raises(TypeError): + cmp("\x00", [1]) def test_rev_wr_with_alphabet(): @@ -640,6 +650,7 @@ def test_wt_lenlex_without_alphabet(): cmp = WtLenLexCmp(weights) assert cmp(x, y) + assert cmp("\x00\x01", "\x02") assert cmp.weights() == weights assert cmp.init([1, 2, 6]) is cmp assert cmp.weights() == [1, 2, 6] @@ -650,6 +661,10 @@ def test_wt_lenlex_without_alphabet(): wt_lenlex_cmp(weights, "\x00", "\x03") with pytest.raises(LibsemigroupsError): cmp([3], [0]) + with pytest.raises(LibsemigroupsError): + cmp("\x00", "\x03") + with pytest.raises(TypeError): + cmp("\x00", [1]) def test_wt_lenlex_with_alphabet(): @@ -715,6 +730,7 @@ def test_rev_wt_lenlex_without_alphabet(): cmp = RevWtLenLexCmp(weights) assert cmp(x, y) + assert cmp("\x01\x00", "\x00\x01") assert cmp.weights() == weights assert cmp.init([2, 1]) is cmp assert cmp.weights() == [2, 1] @@ -724,6 +740,10 @@ def test_rev_wt_lenlex_without_alphabet(): rev_wt_lenlex_cmp(weights, [2], [0]) with pytest.raises(LibsemigroupsError): cmp([2], [0]) + with pytest.raises(LibsemigroupsError): + cmp("\x02", "\x00") + with pytest.raises(TypeError): + cmp("\x00", [1]) def test_rev_wt_lenlex_with_alphabet(): @@ -790,6 +810,7 @@ def test_wt_lex_without_alphabet(): cmp = WtLexCmp(weights) assert cmp(x, y) + assert cmp("\x00\x01", "\x02") assert cmp.weights() == weights assert cmp.init([1, 2, 6]) is cmp assert cmp.weights() == [1, 2, 6] @@ -801,6 +822,10 @@ def test_wt_lex_without_alphabet(): wt_lex_cmp(weights, "\x00", "\x03") with pytest.raises(LibsemigroupsError): cmp([3], [0]) + with pytest.raises(LibsemigroupsError): + cmp("\x00", "\x03") + with pytest.raises(TypeError): + cmp("\x00", [1]) def test_wt_lex_with_alphabet(): @@ -866,6 +891,7 @@ def test_rev_wt_lex_without_alphabet(): cmp = RevWtLexCmp(weights) assert cmp(x, y) + assert cmp("\x01\x00", "\x00\x01") assert cmp.weights() == weights assert cmp.init([2, 1]) is cmp assert cmp.weights() == [2, 1] @@ -876,6 +902,10 @@ def test_rev_wt_lex_without_alphabet(): rev_wt_lex_cmp(weights, [2], [0]) with pytest.raises(LibsemigroupsError): cmp([2], [0]) + with pytest.raises(LibsemigroupsError): + cmp("\x02", "\x00") + with pytest.raises(TypeError): + cmp("\x00", [1]) def test_rev_wt_lex_with_alphabet(): @@ -939,6 +969,7 @@ def test_len_wt_lex_without_alphabet(): cmp = LenWtLexCmp(weights) assert cmp(x, y) + assert cmp("\x00\x01", "\x01\x00") assert cmp.weights() == weights assert cmp.init([2, 1]) is cmp assert cmp.weights() == [2, 1] @@ -948,6 +979,10 @@ def test_len_wt_lex_without_alphabet(): len_wt_lex_cmp(weights, [2], [0]) with pytest.raises(LibsemigroupsError): cmp([2], [0]) + with pytest.raises(LibsemigroupsError): + cmp("\x02", "\x00") + with pytest.raises(TypeError): + cmp("\x00", [1]) def test_len_wt_lex_with_alphabet(): @@ -1011,6 +1046,7 @@ def test_rev_len_wt_lex_without_alphabet(): cmp = RevLenWtLexCmp(weights) assert cmp(x, y) + assert cmp("\x01\x00", "\x00\x01") assert cmp.weights() == weights assert cmp.init([2, 1]) is cmp assert cmp.weights() == [2, 1] @@ -1020,6 +1056,10 @@ def test_rev_len_wt_lex_without_alphabet(): rev_len_wt_lex_cmp(weights, [2], [0]) with pytest.raises(LibsemigroupsError): cmp([2], [0]) + with pytest.raises(LibsemigroupsError): + cmp("\x02", "\x00") + with pytest.raises(TypeError): + cmp("\x00", [1]) def test_rev_len_wt_lex_with_alphabet(): From ea43b3d12b74a06e822f9fff5e84e80c06992cea Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Thu, 20 Aug 2026 12:31:55 +0100 Subject: [PATCH 26/27] order: remove deprecated enum value from doctest --- src/order.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/order.cpp b/src/order.cpp index 2f5ab3d8..3aa986f8 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -1409,8 +1409,8 @@ Reinitialize a {0} object from a {1} list. )pbdoc", name); py::class_ thing(m, - binding_name, - fmt::format(R"pbdoc( + binding_name, + fmt::format(R"pbdoc( Compare words using {2}. Use ``{0}({1})`` to compare either ``str`` or ``list[int]`` words. In this @@ -1438,11 +1438,11 @@ type as *alphabet*. True {3} )pbdoc", - name, - configuration, - ordering, - non_alphabet_string_doctest) - .c_str()); + name, + configuration, + ordering, + non_alphabet_string_doctest) + .c_str()); thing.def(py::init const&, std::vector const&>(), py::arg("alphabet"), @@ -2836,7 +2836,7 @@ Return the alphabet used to compare letters. )pbdoc", py::return_value_policy::reference_internal); } // bind_rev_lenlex_cmp_with_alphabet - } // namespace + } // namespace void init_order(py::module& m) { py::options options; @@ -2905,8 +2905,6 @@ respectively, in new code. >>> from libsemigroups_pybind11 import Order >>> Order.lenlex - >>> Order.shortlex == Order.lenlex - True )pbdoc") .value("none", Order::none) .value("lenlex", Order::lenlex) From ae2663e6b11bba05b4dd77ed801e6eff761ddcb4 Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Thu, 20 Aug 2026 12:34:01 +0100 Subject: [PATCH 27/27] lint + format --- src/order.cpp | 16 ++++++++-------- tests/test_order.py | 6 ++++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/order.cpp b/src/order.cpp index 3aa986f8..e928e1a4 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -1409,8 +1409,8 @@ Reinitialize a {0} object from a {1} list. )pbdoc", name); py::class_ thing(m, - binding_name, - fmt::format(R"pbdoc( + binding_name, + fmt::format(R"pbdoc( Compare words using {2}. Use ``{0}({1})`` to compare either ``str`` or ``list[int]`` words. In this @@ -1438,11 +1438,11 @@ type as *alphabet*. True {3} )pbdoc", - name, - configuration, - ordering, - non_alphabet_string_doctest) - .c_str()); + name, + configuration, + ordering, + non_alphabet_string_doctest) + .c_str()); thing.def(py::init const&, std::vector const&>(), py::arg("alphabet"), @@ -2836,7 +2836,7 @@ Return the alphabet used to compare letters. )pbdoc", py::return_value_policy::reference_internal); } // bind_rev_lenlex_cmp_with_alphabet - } // namespace + } // namespace void init_order(py::module& m) { py::options options; diff --git a/tests/test_order.py b/tests/test_order.py index a70612ad..80756759 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -4,6 +4,8 @@ # # The full license is in the file LICENSE, distributed with this software. +# pylint: disable=too-many-lines, invalid-name + """Tests for order.cpp.""" from copy import copy @@ -60,7 +62,7 @@ def _check_simple_copy_and_repr(Cmp, name): original.__copy__(), # pylint: disable=unnecessary-dunder-call copy(original), ) - assert all(type(copied) is Cmp for copied in copies) + assert all(isinstance(copied, Cmp) for copied in copies) assert all(copied is not original for copied in copies) assert all(copied.py_template_params == original.py_template_params for copied in copies) assert all(copied(x, y) for copied in copies) @@ -90,7 +92,7 @@ def _check_configured_copy_and_repr(Cmp, name, configuration_name, accessor_name copy(original), ) original.init(*replacement_args) - assert all(type(copied) is Cmp for copied in copies) + assert all(isinstance(copied, Cmp) for copied in copies) assert all(copied is not original for copied in copies) assert all(getattr(copied, accessor_name)() == [1, 2] for copied in copies) if alphabet is not None: