From c7563a4a87af85147f0b07bdc9727798320d5334 Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Fri, 14 Aug 2026 16:23:43 +0100 Subject: [PATCH 1/3] order: add wt_lex_cmp + wt_lenlex_cmp --- docs/source/data-structures/order/index.rst | 6 + src/libsemigroups_pybind11/__init__.py | 15 +- src/order.cpp | 152 ++++++++++++++++++++ tests/test_order.py | 40 ++++++ 4 files changed, 212 insertions(+), 1 deletion(-) diff --git a/docs/source/data-structures/order/index.rst b/docs/source/data-structures/order/index.rst index c8720c1a..aeecf638 100644 --- a/docs/source/data-structures/order/index.rst +++ b/docs/source/data-structures/order/index.rst @@ -37,6 +37,8 @@ Contents rev_rpo_cmp rpo_cmp wr_cmp + wt_lenlex_cmp + wt_lex_cmp Full API -------- @@ -50,3 +52,7 @@ Full API .. autofunction:: rpo_cmp .. autofunction:: wr_cmp + +.. autofunction:: wt_lenlex_cmp + +.. autofunction:: wt_lex_cmp diff --git a/src/libsemigroups_pybind11/__init__.py b/src/libsemigroups_pybind11/__init__.py index acae42b2..6fa37370 100644 --- a/src/libsemigroups_pybind11/__init__.py +++ b/src/libsemigroups_pybind11/__init__.py @@ -56,7 +56,14 @@ from .order import 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 @@ -120,6 +127,8 @@ shortlex_compare, side, tril, + wt_lenlex_cmp as _wt_lenlex_cmp, + wt_lex_cmp as _wt_lex_cmp, ) except ModuleNotFoundError as e: raise ModuleNotFoundError( @@ -132,6 +141,8 @@ lex_cmp = _wrap_cxx_free_fn(_lex_cmp) rev_rpo_cmp = _wrap_cxx_free_fn(_rev_rpo_cmp) rpo_cmp = _wrap_cxx_free_fn(_rpo_cmp) +wt_lenlex_cmp = _wrap_cxx_free_fn(_wt_lenlex_cmp) +wt_lex_cmp = _wrap_cxx_free_fn(_wt_lex_cmp) __all__ = [ @@ -186,6 +197,8 @@ "side", "tril", "wr_cmp", + "wt_lenlex_cmp", + "wt_lex_cmp", # Submodules "action", "adapters", diff --git a/src/order.cpp b/src/order.cpp index 784cdd5e..35925d5c 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -175,6 +175,7 @@ compared by their positions in *alphabet*. :type y: str | list[int] :returns: Whether *x* is less than *y*. :rtype: bool + :raises LibsemigroupsError: if either word contains a letter that does not belong to *alphabet*. @@ -326,6 +327,157 @@ left, with letters compared by their positions in *alphabet*. True )pbdoc"); + m.def( + "wt_lenlex_cmp", + [](std::vector const& weights, Word const& x, Word const& y) { + return wt_lenlex_cmp(weights, x, y); + }, + py::arg("weights"), + py::arg("x"), + py::arg("y"), + R"pbdoc( +:sig=(weights: list[int], x: str | list[int], y: str | list[int]) -> bool: +:only-document-once: +Compare two words using weighted len-lex ordering. + +The *i*-th entry of *weights* is the weight assigned to generator *i*. +Words are first ordered by their total weight, then by length, and finally +lexicographically. + +:param weights: the weight assigned to each generator. +:type weights: list[int] +: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 LibsemigroupsError: if a letter is not a valid index into *weights*. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import wt_lenlex_cmp + >>> wt_lenlex_cmp([1, 2], [1], [0, 0]) + True +)pbdoc"); + + m.def( + "wt_lenlex_cmp", + [](Alphabet const& alphabet, + std::vector const& weights, + Word const& x, + Word const& y) { return wt_lenlex_cmp(alphabet, weights, x, y); }, + py::arg("alphabet"), + py::arg("weights"), + py::arg("x"), + py::arg("y"), + R"pbdoc( +:sig=(alphabet: Alphabet, weights: list[int], x: str | list[int], y: str | list[int]) -> bool: +:only-document-once: +Compare two words using alphabet-aware weighted len-lex ordering. + +Letters are mapped to their positions in *alphabet*, and the *i*-th entry of +*weights* is the weight assigned to the *i*-th letter of *alphabet*. Words are +first ordered by their total weight, then by length, and finally +lexicographically according to *alphabet*. + +:param alphabet: the ordered alphabet containing the letters of both words. +:type alphabet: Alphabet +:param weights: the weight assigned to each letter of *alphabet*. +:type weights: list[int] +: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 LibsemigroupsError: if a letter does not belong to *alphabet*, or its + position in *alphabet* is not a valid index into *weights*. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, wt_lenlex_cmp + >>> wt_lenlex_cmp(Alphabet("ba"), [1, 1], "b", "a") + True +)pbdoc"); + + m.def( + "wt_lex_cmp", + [](std::vector const& weights, Word const& x, Word const& y) { + return wt_lex_cmp(weights, x, y); + }, + py::arg("weights"), + py::arg("x"), + py::arg("y"), + R"pbdoc( +:sig=(weights: list[int], x: str | list[int], y: str | list[int]) -> bool: +:only-document-once: +Compare two words using weighted lexicographic ordering. + +The *i*-th entry of *weights* is the weight assigned to generator *i*. +Words are first ordered by their total weight and then lexicographically. + +:param weights: the weight assigned to each generator. +:type weights: list[int] +: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 LibsemigroupsError: if a letter is not a valid index into *weights*. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import wt_lex_cmp + >>> wt_lex_cmp([1, 2], [1], [0, 0]) + False +)pbdoc"); + + m.def( + "wt_lex_cmp", + [](Alphabet const& alphabet, + std::vector const& weights, + Word const& x, + Word const& y) { return wt_lex_cmp(alphabet, weights, x, y); }, + py::arg("alphabet"), + py::arg("weights"), + py::arg("x"), + py::arg("y"), + R"pbdoc( +:sig=(alphabet: Alphabet, weights: list[int], x: str | list[int], y: str | list[int]) -> bool: +:only-document-once: +Compare two words using alphabet-aware weighted lexicographic ordering. + +Letters are mapped to their positions in *alphabet*, and the *i*-th entry of +*weights* is the weight assigned to the *i*-th letter of *alphabet*. Words are +first ordered by their total weight and then lexicographically according to +*alphabet*. + +:param alphabet: the ordered alphabet containing the letters of both words. +:type alphabet: Alphabet +:param weights: the weight assigned to each letter of *alphabet*. +:type weights: list[int] +: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 LibsemigroupsError: if a letter does not belong to *alphabet*, or its + position in *alphabet* is not a valid index into *weights*. + +.. doctest:: python + + >>> from libsemigroups_pybind11 import Alphabet, wt_lex_cmp + >>> wt_lex_cmp(Alphabet("ba"), [1, 1], "b", "a") + True +)pbdoc"); + bind_deprecated_compare( m, "lexicographical_compare", diff --git a/tests/test_order.py b/tests/test_order.py index b2e82d68..26c8105d 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -20,6 +20,8 @@ rpo_cmp, shortlex_compare, wr_cmp, + wt_lenlex_cmp, + wt_lex_cmp, ) @@ -102,3 +104,41 @@ def test_wr_cmp_with_alphabet(): with pytest.raises(LibsemigroupsError): wr_cmp(alphabet, levels, "d", "b") + + +@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] + + assert compare(weights, [0, 1], [2]) + assert not compare(weights, [2], [0, 1]) + + with pytest.raises(LibsemigroupsError): + compare(weights, [0], [3]) + + +def test_weighted_comparisons_use_different_tie_breakers(): + """Check the length and lexicographic tie breakers differ.""" + weights = [1, 2] + + assert wt_lenlex_cmp(weights, [1], [0, 0]) + assert not wt_lex_cmp(weights, [1], [0, 0]) + + +@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") + + word_alphabet = Alphabet([1, 0]) + assert compare(word_alphabet, [1, 1], [1], [0]) + + with pytest.raises(LibsemigroupsError): + compare(alphabet, [1, 1], "c", "a") + + with pytest.raises(LibsemigroupsError): + compare(alphabet, [1], "a", "b") From 0462cd54d1ce653876ea431a911a8b293e19c9a3 Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Tue, 18 Aug 2026 15:52:51 +0100 Subject: [PATCH 2/3] Updates from code review --- src/order.cpp | 14 +++++++------- tests/test_order.py | 6 ++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/order.cpp b/src/order.cpp index 35925d5c..0e9a46fa 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -358,8 +358,8 @@ lexicographically. .. doctest:: python >>> from libsemigroups_pybind11 import wt_lenlex_cmp - >>> wt_lenlex_cmp([1, 2], [1], [0, 0]) - True + >>> wt_lenlex_cmp([1, 3], [1], [0, 0]) + False )pbdoc"); m.def( @@ -399,8 +399,8 @@ lexicographically according to *alphabet*. .. doctest:: python >>> from libsemigroups_pybind11 import Alphabet, wt_lenlex_cmp - >>> wt_lenlex_cmp(Alphabet("ba"), [1, 1], "b", "a") - True + >>> wt_lenlex_cmp(Alphabet("ba"), [3, 1], "b", "a") + False )pbdoc"); m.def( @@ -433,8 +433,8 @@ Words are first ordered by their total weight and then lexicographically. .. doctest:: python >>> from libsemigroups_pybind11 import wt_lex_cmp - >>> wt_lex_cmp([1, 2], [1], [0, 0]) - False + >>> wt_lex_cmp([1, 1], [1], [0, 0]) + True )pbdoc"); m.def( @@ -474,7 +474,7 @@ first ordered by their total weight and then lexicographically according to .. doctest:: python >>> from libsemigroups_pybind11 import Alphabet, wt_lex_cmp - >>> wt_lex_cmp(Alphabet("ba"), [1, 1], "b", "a") + >>> wt_lex_cmp(Alphabet("ba"), [3, 1], "a", "b") True )pbdoc"); diff --git a/tests/test_order.py b/tests/test_order.py index 26c8105d..a008ee7e 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -117,6 +117,12 @@ def test_weighted_comparisons_for_integer_words(compare): with pytest.raises(LibsemigroupsError): compare(weights, [0], [3]) + assert compare(weights, chr(0) + chr(1), chr(2)) + assert not compare(weights, chr(2), chr(0) + chr(1)) + + with pytest.raises(LibsemigroupsError): + compare(weights, chr(0), chr(3)) + def test_weighted_comparisons_use_different_tie_breakers(): """Check the length and lexicographic tie breakers differ.""" From 60baac9b45030f7f75b805f1f5ae7625383c108f Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Tue, 18 Aug 2026 15:57:25 +0100 Subject: [PATCH 3/3] Format --- src/libsemigroups_pybind11/__init__.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/libsemigroups_pybind11/__init__.py b/src/libsemigroups_pybind11/__init__.py index 6fa37370..e6128171 100644 --- a/src/libsemigroups_pybind11/__init__.py +++ b/src/libsemigroups_pybind11/__init__.py @@ -56,14 +56,7 @@ from .order import 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