diff --git a/docs/source/data-structures/order/index.rst b/docs/source/data-structures/order/index.rst index 6f371ce9..78fcd1c0 100644 --- a/docs/source/data-structures/order/index.rst +++ b/docs/source/data-structures/order/index.rst @@ -30,6 +30,8 @@ Contents rev_rpo_cmp rpo_cmp shortlex_compare + wt_lenlex_cmp + wt_lex_cmp Full API -------- @@ -47,3 +49,7 @@ Full API .. autofunction:: rpo_cmp .. autofunction:: shortlex_compare + +.. autofunction:: wt_lenlex_cmp + +.. autofunction:: wt_lex_cmp diff --git a/src/libsemigroups_pybind11/__init__.py b/src/libsemigroups_pybind11/__init__.py index 4d15bbd4..d1993a0d 100644 --- a/src/libsemigroups_pybind11/__init__.py +++ b/src/libsemigroups_pybind11/__init__.py @@ -117,6 +117,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( @@ -129,6 +131,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__ = [ @@ -182,6 +186,8 @@ "shortlex_compare", "side", "tril", + "wt_lenlex_cmp", + "wt_lex_cmp", # Submodules "action", "adapters", diff --git a/src/order.cpp b/src/order.cpp index 8e05b195..b6617fd8 100644 --- a/src/order.cpp +++ b/src/order.cpp @@ -18,6 +18,7 @@ // C++ stl headers.... #include // for string +#include // for vector // libsemigroups.... #include // for *_cmp, Order @@ -137,6 +138,157 @@ word contains a letter that does not belong to *alphabet*. :rtype: bool )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_compare( m, "rpo_cmp", diff --git a/tests/test_order.py b/tests/test_order.py index 5805036f..b0e28fd5 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -19,6 +19,8 @@ rev_rpo_cmp, rpo_cmp, shortlex_compare, + wt_lenlex_cmp, + wt_lex_cmp, ) @@ -73,3 +75,41 @@ def test_deprecated_comparisons(old_compare, new_compare): with pytest.deprecated_call(): result = old_compare("ab", "ba") assert result == new_compare("ab", "ba") + + +@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")