From ec170d8f873c1982ef884d449191daee6e2e410e Mon Sep 17 00:00:00 2001 From: Joseph Edwards Date: Sat, 9 May 2026 11:50:12 +0100 Subject: [PATCH 1/7] presentation: add add_commutator_rule --- src/present.cpp | 150 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/src/present.cpp b/src/present.cpp index a74f0b0a..34b7616a 100644 --- a/src/present.cpp +++ b/src/present.cpp @@ -565,6 +565,156 @@ alphabet of *p*, and where :math:`z` is the second parameter. :raises LibsemigroupsError: if *z* is not a letter in ``p.alphabet()``. :complexity: Linear in the number of rules.)pbdoc"); + m.def( + "presentation_add_commutator_rule", + [](Presentation_& p, + Word const& x, + Word const& y, + Word const& alphabet, + Word const& inverses, + typename Presentation_::letter_type id) { + return presentation::add_commutator_rule( + p, x, y, alphabet, inverses, id); + }, + py::arg("p"), + py::arg("x"), + py::arg("y"), + py::arg("alphabet"), + py::arg("inverses"), + py::kw_only(), // This is so id must be specified by a key-word + py::arg("id") + = static_cast(UNDEFINED), + R"pbdoc( +Add a commutator rule. + +Adds the rule :math:`x^{-1}y^{-1}xy = id` to *p*. The letter ``a`` with index +``i`` in *inverses* is the inverse of the letter in *alphabet* with index +``i``. If *id* is :any:`UNDEFINED`, then the right-hand side is the empty +word. + +:param p: the presentation. +:type p: Presentation + +:param x: the first word in the commutator. +:type x: :ref:`Word` + +:param y: the second word in the commutator. +:type y: :ref:`Word` + +:param alphabet: + the alphabet, which should be a superset of the letters in *x* and *y*. +:type alphabet: :ref:`Word` + +:param inverses: the inverses of the letters in alphabet. +:type inverses: :ref:`Word` + +:param id: the identity letter, or UNDEFINED for the empty word. +:type id: :ref:`Letter` + +:raises LibsemigroupsError: + if *alphabet*, *inverses*, *x*, *y*, or *id* contains a letter not + belonging to ``p.alphabet()``; if *alphabet* contains duplicates; if + *inverses* are not valid inverses for *alphabet*; or if *x* or *y* + contains a letter not belonging to *alphabet*. + +.. seealso:: + * :any:`throw_if_contains_duplicates`, + * :any:`throw_if_bad_inverses`, + * :any:`throw_if_word_not_over_alphabet` and + * :any:`throw_if_letter_not_in_alphabet`. +)pbdoc"); + m.def( + "presentation_add_commutator_rule", + [](Presentation_& p, + Word const& x, + Word const& y, + Word const& inverses, + typename Presentation_::letter_type id) { + return presentation::add_commutator_rule(p, x, y, inverses, id); + }, + py::arg("p"), + py::arg("x"), + py::arg("y"), + py::arg("inverses"), + py::kw_only(), // This is so id must be specified by a key-word + py::arg("id") + = static_cast(UNDEFINED), + R"pbdoc( +Add a commutator rule. + +Adds the rule :math:`x^{-1}y^{-1}xy = id` to *p*. The letter ``a`` with index +``i`` in *inverses* is the inverse of the letter in ``p.alphabet()`` with +index ``i``. If *id* is :any:`UNDEFINED`, then the right-hand side is the +empty word. + +:param p: the presentation. +:type p: Presentation + +:param x: the first word in the commutator. +:type x: :ref:`Word` + +:param y: the second word in the commutator. +:type y: :ref:`Word` + +:param inverses: the inverses of the letters in p.alphabet(). +:type inverses: :ref:`Word` + +:param id: the identity letter, or UNDEFINED for the empty word. +:type id: :ref:`Letter` + + +:raises LibsemigroupsError: + if *inverses*, *x*, *y*, or *id* contains a letter not belonging to + ``p.alphabet()``, or if *inverses* are not valid inverses for + ``p.alphabet()``. + +.. seealso:: + * :any:`throw_if_bad_inverses` and + * :any:`throw_if_letter_not_in_alphabet`. +)pbdoc"); + m.def( + "presentation_add_commutator_rule", + [](Presentation_& p, + Word const& x, + Word const& y, + typename Presentation_::letter_type id) { + return presentation::add_commutator_rule(p, x, y, id); + }, + py::arg("p"), + py::arg("x"), + py::arg("y"), + py::kw_only(), // This is so id must be specified by a key-word + py::arg("id") + = static_cast(UNDEFINED), + R"pbdoc( +Add a commutator rule. + +Adds the rule :math:`x^{-1}y^{-1}xy = id` to *p*, after attempting to detect +inverses from the rules in *p*, using :any:`try_detect_inverses`. If +*id* is :any:`UNDEFINED`, then the right-hand side is the empty word. + +:param p: the presentation. +:type p: Presentation + +:param x: the first word in the commutator. +:type x: :ref:`Word` + +:param y: the second word in the commutator. +:type y: :ref:`Word` + +:param id: the identity letter, or UNDEFINED for the empty word. +:type id: :ref:`Letter` + +:raises LibsemigroupsError: + if *x*, *y*, or *id* contains a letter not belonging to + ``p.alphabet()``, if :any:`try_detect_inverses` throws, or if *x* or + *y* contains a letter for which no inverse was detected. + +.. seealso:: + * :any:`throw_if_letter_not_in_alphabet`, + * :any:`try_detect_inverses` and + * :any:`throw_if_word_not_over_alphabet`. +)pbdoc"); m.def( "presentation_are_rules_sorted", [](Presentation_ const& p) { From e8f1567d816d2210093be3a1bbd4dc6bcc448b9f Mon Sep 17 00:00:00 2001 From: Joseph Edwards Date: Sat, 9 May 2026 15:23:23 +0100 Subject: [PATCH 2/7] presentation: add commutator --- src/present.cpp | 123 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/src/present.cpp b/src/present.cpp index 34b7616a..5b810a97 100644 --- a/src/present.cpp +++ b/src/present.cpp @@ -565,6 +565,129 @@ alphabet of *p*, and where :math:`z` is the second parameter. :raises LibsemigroupsError: if *z* is not a letter in ``p.alphabet()``. :complexity: Linear in the number of rules.)pbdoc"); + m.def( + "presentation_commutator", + [](Presentation const& p, Word const& x, Word const& y) { + return presentation::commutator(p, x, y); + }, + py::arg("p"), + py::arg("x"), + py::arg("y"), + R"pbdoc( +Return the commutator of two words. + +Returns the word :math:`x^{-1}y^{-1}xy`, after attempting to detect inverses +from the rules in *p*, using :any:`try_detect_inverses`. + +:param p: the presentation. +:type p: Presentation + +:param x: the first word in the commutator. +:type x: :ref:`Word` + +:param y: the second word in the commutator. +:type y: :ref:`Word` + +:returns: The commutator :math:`x^{-1}y^{-1}xy`. +:rtype: :ref:`Word` + +:raises LibsemigroupsError: + if *x* or *y* contains a letter not belonging to ``p.alphabet()``, if + :any:`try_detect_inverses` throws, or if *x* or *y* contains a letter for + which no inverse was detected. + +.. seealso:: + * :any:`try_detect_inverses` and + * :any:`throw_if_letter_not_in_alphabet`. + + + +)pbdoc"); + m.def( + "presentation_commutator", + [](Presentation const& p, + Word const& x, + Word const& y, + Word const& inverses) { + return presentation::commutator(p, x, y, inverses); + }, + py::arg("p"), + py::arg("x"), + py::arg("y"), + py::arg("inverses"), + R"pbdoc( +Return the commutator of two words. + +Returns the word :math:`x^{-1}y^{-1}xy`. The letter ``a`` with index ``i`` in +*inverses* is the inverse of the letter in ``p.alphabet()`` with index ``i``. + +:param p: the presentation. +:type p: Presentation + +:param x: the first word in the commutator. +:type x: :ref:`Word` + +:param y: the second word in the commutator. +:type y: :ref:`Word` + +:param inverses: the inverses of the letters in p.alphabet(). +:type inverses: :ref:`Word` + +:returns: The commutator :math:`x^{-1}y^{-1}xy`. +:rtype: :ref:`Word` + +:raises LibsemigroupsError: + if *inverses* are not valid inverses for ``p.alphabet()``, or if *x* or + *y* contains a letter not belonging to ``p.alphabet()``. + +.. seealso:: + * :any:`throw_if_bad_inverses` and + * :any:`throw_if_letter_not_in_alphabet`. +)pbdoc"); + m.def( + "presentation_commutator", + [](Word const& x, + Word const& y, + Word const& alphabet, + Word const& inverses) { + return presentation::commutator(x, y, alphabet, inverses); + }, + py::arg("x"), + py::arg("y"), + py::arg("alphabet"), + py::arg("inverses"), + R"pbdoc( +Return the commutator of two words. + +Returns the word :math:`x^{-1}y^{-1}xy`. The letter ``a`` with index ``i`` in +*inverses* is the inverse of the letter in *alphabet* with index ``i``. + +:param x: the first word in the commutator. +:type x: :ref:`Word` + +:param y: the second word in the commutator. +:type y: :ref:`Word` + +:param alphabet: + the alphabet, which should be a superset of the letters in x and y. +:type alphabet: :ref:`Word` + +:param inverses: the inverses of the letters in alphabet. +:type inverses: :ref:`Word` + +:returns: The commutator :math:`x^{-1}y^{-1}xy`. +:rtype: :ref:`Word` + +:raises LibsemigroupsError: + if *alphabet* contains duplicates, if *inverses* are not valid inverses + for *alphabet*, or if *x* or *y* contains a letter not belonging to + *alphabet*. + +.. seealso:: + * :any:`throw_if_contains_duplicates`, + * :any:`throw_if_bad_inverses` and + * :any:`throw_if_word_not_over_alphabet`. +)pbdoc"); m.def( "presentation_add_commutator_rule", [](Presentation_& p, From 9d892fd8fac7f50fb96a4df665cee9653dbe7d90 Mon Sep 17 00:00:00 2001 From: Joseph Edwards Date: Sat, 9 May 2026 22:55:19 +0100 Subject: [PATCH 3/7] presentation: expose commutator functions --- src/libsemigroups_pybind11/presentation/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libsemigroups_pybind11/presentation/__init__.py b/src/libsemigroups_pybind11/presentation/__init__.py index a916b1d2..78232d8d 100644 --- a/src/libsemigroups_pybind11/presentation/__init__.py +++ b/src/libsemigroups_pybind11/presentation/__init__.py @@ -13,6 +13,7 @@ InversePresentationWord as _InversePresentationWord, PresentationString as _PresentationString, PresentationWord as _PresentationWord, + presentation_add_commutator_rule as _add_commutator_rule, presentation_add_cyclic_conjugates as _add_cyclic_conjugates, presentation_add_identity_rules as _add_identity_rules, presentation_add_inverse_rules as _add_inverse_rules, @@ -22,6 +23,7 @@ presentation_are_rules_sorted as _are_rules_sorted, presentation_balance as _balance, presentation_change_alphabet as _change_alphabet, + presentation_commutator as _commutator, presentation_contains_rule as _contains_rule, presentation_first_unused_letter as _first_unused_letter, presentation_greedy_reduce_length as _greedy_reduce_length, @@ -227,6 +229,7 @@ def __init__(self: _Self, *args, **kwargs) -> None: ######################################################################## +add_commutator_rule = _wrap_cxx_free_fn(_add_commutator_rule) add_identity_rules = _wrap_cxx_free_fn(_add_identity_rules) add_inverse_rules = _wrap_cxx_free_fn(_add_inverse_rules) add_rule = _wrap_cxx_free_fn(_add_rule) @@ -234,6 +237,7 @@ def __init__(self: _Self, *args, **kwargs) -> None: add_zero_rules = _wrap_cxx_free_fn(_add_zero_rules) are_rules_sorted = _wrap_cxx_free_fn(_are_rules_sorted) change_alphabet = _wrap_cxx_free_fn(_change_alphabet) +commutator = _wrap_cxx_free_fn(_commutator) contains_rule = _wrap_cxx_free_fn(_contains_rule) first_unused_letter = _wrap_cxx_free_fn(_first_unused_letter) greedy_reduce_length = _wrap_cxx_free_fn(_greedy_reduce_length) From a51966748a31270f89ad1929ce1171607cdd3f2a Mon Sep 17 00:00:00 2001 From: Joseph Edwards Date: Sat, 9 May 2026 22:56:00 +0100 Subject: [PATCH 4/7] presentation: add tests --- tests/test_present.py | 294 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) diff --git a/tests/test_present.py b/tests/test_present.py index 65a0d1a0..f6936a35 100644 --- a/tests/test_present.py +++ b/tests/test_present.py @@ -194,6 +194,280 @@ def check_add_identity_rules(W): ] +def check_commutator_errors(W): + # Alphabet specified, inverses specified + + # Words not over the alphabet + with pytest.raises(LibsemigroupsError): + presentation.commutator(W([0]), W([]), W([]), W([])) + with pytest.raises(LibsemigroupsError): + presentation.commutator(W([]), W([0]), W([]), W([])) + + # Incorrect number of inverses + with pytest.raises(LibsemigroupsError): + presentation.commutator(W([]), W([]), W([0]), W([])) + with pytest.raises(LibsemigroupsError): + presentation.commutator(W([]), W([]), W([]), W([0])) + + # Alphabet and inverses contain duplicates + with pytest.raises(LibsemigroupsError): + presentation.commutator(W([]), W([]), W([0, 0]), W([0, 1])) + with pytest.raises(LibsemigroupsError): + presentation.commutator(W([]), W([]), W([0, 1]), W([0, 0])) + + # Invalid inverses + with pytest.raises(LibsemigroupsError): + presentation.commutator(W([]), W([]), W([0, 1, 2]), W([1, 2, 0])) + + # Alphabet inferred, inverses specified + + p = Presentation(W([])) + p.contains_empty_word(True) + + # Words not over the presentation's alphabet + p.alphabet(W([0])) + with pytest.raises(LibsemigroupsError): + presentation.commutator(p, W([1]), W([]), W([0])) + with pytest.raises(LibsemigroupsError): + presentation.commutator(p, W([]), W([1]), W([0])) + + # Incorrect number of inverses + with pytest.raises(LibsemigroupsError): + presentation.commutator(p, W([]), W([]), W([])) + + # Inverses contain letters not in the presentation's alphabet + with pytest.raises(LibsemigroupsError): + presentation.commutator(p, W([]), W([]), W([1])) + + # Inverses contain duplicates + p.alphabet(W([0, 1])) + with pytest.raises(LibsemigroupsError): + presentation.commutator(p, W([]), W([]), W([0, 0])) + + # Invalid duplicates + p.alphabet(W([0, 1, 2])) + with pytest.raises(LibsemigroupsError): + presentation.commutator(p, W([]), W([]), W([1, 2, 0])) + + # Alphabet inferred, inverses inferred + + p.init() + p.alphabet(W([])) + p.contains_empty_word(True) + + p.alphabet(W([0])) + # Words not over presentation's alphabet + with pytest.raises(LibsemigroupsError): + presentation.commutator(p, W([1]), W([])) + with pytest.raises(LibsemigroupsError): + presentation.commutator(p, W([]), W([1])) + + p.alphabet(W([0, 1])) + presentation.add_rule(p, W([0, 0]), W([])) + presentation.add_rule(p, W([0, 1]), W([])) + # The rules provided don't provide consistent inverses + with pytest.raises(LibsemigroupsError): + presentation.commutator(p, W([]), W([])) + + p.init() + p.contains_empty_word(True) + p.alphabet(W([0, 1, 2])) + + # 0 and 1 have inverses, but 2 does not + presentation.add_rule(p, W([0, 1]), W([])) + presentation.add_rule(p, W([1, 0]), W([])) + + # Words are not over the letters that have inverses + with pytest.raises(LibsemigroupsError): + presentation.commutator(p, W([0, 1, 2]), W([1, 2])) + + +def check_commutator(W): + p = Presentation(W([])) + p.contains_empty_word(True) + + # alphabet specified, inverses specified + assert presentation.commutator(W([]), W([]), W([]), W([])) == W([]) + + assert presentation.commutator(W([0]), W([]), W([0]), W([1])) == W([1, 0]) + assert presentation.commutator(W([]), W([0]), W([0]), W([1])) == W([1, 0]) + assert presentation.commutator(W([0, 1]), W([]), W([0, 1]), W([2, 3])) == W([3, 2, 0, 1]) + assert presentation.commutator(W([]), W([0, 1]), W([0, 1]), W([2, 3])) == W([3, 2, 0, 1]) + assert presentation.commutator(W([]), W([0, 1]), W([0, 1]), W([1, 0])) == W([0, 1, 0, 1]) + assert presentation.commutator(W([0, 1]), W([]), W([0, 1]), W([1, 0])) == W([0, 1, 0, 1]) + + assert presentation.commutator(W([0, 1, 2]), W([1, 0, 1]), W([0, 1, 2]), W([3, 4, 5])) == W( + [5, 4, 3, 4, 3, 4, 0, 1, 2, 1, 0, 1] + ) + assert presentation.commutator(W([0, 1, 2]), W([1, 0, 1]), W([0, 1, 2]), W([2, 1, 0])) == W( + [0, 1, 2, 1, 2, 1, 0, 1, 2, 1, 0, 1] + ) + + # alphabet inferred, inverses specified + assert presentation.commutator(p, W([]), W([]), W([])) == W([]) + + p.alphabet(W([0, 1, 2])) + assert presentation.commutator(p, W([0, 1]), W([1]), W([1, 0, 2])) == W([0, 1, 0, 0, 1, 1]) + assert presentation.commutator(p, W([0, 1]), W([1]), W([0, 1, 2])) == W([1, 0, 1, 0, 1, 1]) + assert presentation.commutator(p, W([0, 1]), W([1]), W([0, 2, 1])) == W([2, 0, 2, 0, 1, 1]) + + # alphabet inferred, inverses inferred + assert presentation.commutator(p, W([]), W([])) == W([]) + + p.alphabet(W([0, 1, 2])) + presentation.add_rule(p, W([0, 2]), W([])) + presentation.add_rule(p, W([2, 0]), W([])) + assert presentation.commutator(p, W([0, 0]), W([2])) == W([2, 2, 0, 0, 0, 2]) + + +def check_add_commutator_rule_errors(W): + p = Presentation(W([])) + + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([0]), W([]), W([]), W([])) + + p.contains_empty_word(True) + p.alphabet(W([0])) + + # The words are not over the provided alphabet + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([0]), W([]), W([]), W([])) + + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([]), W([0]), W([]), W([])) + + # The words are not over the presentation's alphabet + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([1]), W([]), W([0]), W([0])) + + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([]), W([1]), W([0]), W([0])) + + # The provided alphabet and inverses are not over the presentation's + # alphabet + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([]), W([]), W([1]), W([])) + + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([]), W([]), W([]), W([1])) + + # Alphabet and inverses contain duplicates + p.alphabet(W([0, 1])) + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([]), W([]), W([0, 0]), W([0, 1])) + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([]), W([]), W([0, 1]), W([0, 0])) + + # The inverses are not valid + p.alphabet(W([0, 1, 2])) + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([]), W([]), W([0, 1, 2]), W([1, 2, 0])) + + # The id is not in the presentation's alphabet + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([]), W([]), W([0, 1, 2]), W([2, 1, 0]), id=W([4])[0]) + + p.init() + p.contains_empty_word(True) + p.alphabet(W([0])) + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([0]), W([]), W([])) + + p.alphabet(W([0])) + # The words are not over the presentation's alphabet + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([1]), W([]), W([0])) + presentation.add_commutator_rule(p, W([]), W([1]), W([0])) + + # The provided inverses are not over the presentation's alphabet + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([]), W([]), W([1])) + + # Inverses contain duplicates + p.alphabet(W([0, 1])) + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([]), W([]), W([0, 0])) + + # The inverses are not valid + p.alphabet(W([0, 1, 2])) + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([]), W([]), W([1, 2, 0])) + + # The id is not in the presentation's alphabet + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([]), W([]), W([2, 1, 0]), id=W([4])[0]) + + p.contains_empty_word(True) + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([0]), W([])) + + p.init() + p.contains_empty_word(True) + p.alphabet(W([0])) + # The words are not over the presentation's alphabet + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([1]), W([])) + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([]), W([1])) + + p.alphabet(W([0, 1])) + presentation.add_rule(p, W([0, 0]), W([])) + presentation.add_rule(p, W([0, 1]), W([])) + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([]), W([])) + + p.init() + p.contains_empty_word(True) + p.alphabet(W([0, 1, 2])) + # 0 and 1 have inverses, but 2 does not + presentation.add_rule(p, W([0, 1]), W([])) + presentation.add_rule(p, W([1, 0]), W([])) + + # The words are not over the subset of the presentation's alphabet that + # has inverses + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([0, 1, 2]), W([1, 2])) + + # The id is not in the presentation's alphabet + with pytest.raises(LibsemigroupsError): + presentation.add_commutator_rule(p, W([]), W([]), id=W([3])[0]) + + +def check_add_commutator_rule(W): + p = Presentation(W([0, 1, 2, 3])) + p.contains_empty_word(True) + presentation.add_commutator_rule(p, W([0]), W([1]), W([0, 1]), W([2, 3])) + presentation.add_commutator_rule(p, W([2, 0]), W([1]), W([2, 1, 0]), W([0, 3, 2]), id=W([0])[0]) + + assert p.rules == [W([2, 3, 0, 1]), W([]), W([2, 0, 3, 2, 0, 1]), W([0])] + + p.init() + p.contains_empty_word(True) + p.alphabet(W([0, 1, 2, 3])) + presentation.add_commutator_rule(p, W([0]), W([1]), W([2, 3, 0, 1])) + presentation.add_commutator_rule(p, W([2, 0]), W([1]), W([2, 3, 0, 1]), id=W([0])[0]) + + assert p.rules == [W([2, 3, 0, 1]), W([]), W([2, 0, 3, 2, 0, 1]), W([0])] + + p.init() + p.contains_empty_word(True) + p.alphabet(W([0, 1, 2, 3])) + presentation.add_rule(p, W([0, 2]), W([])) + presentation.add_rule(p, W([2, 0]), W([])) + presentation.add_commutator_rule(p, W([2, 0]), W([0])) + presentation.add_commutator_rule(p, W([2, 0]), W([0]), id=W([0])[0]) + assert p.rules == [ + W([0, 2]), + W([]), + W([2, 0]), + W([]), + W([2, 0, 2, 2, 0, 0]), + W([]), + W([2, 0, 2, 2, 0, 0]), + W([0]), + ] + + def check_add_inverse_rules(W): p = Presentation(W([0, 1, 2])) presentation.add_rule(p, W([0, 1, 2, 1]), W([0, 0])) @@ -1353,3 +1627,23 @@ def test_presentation_try_detect_inverses(): ) letters, inverses = "".join(letters), "".join(inverses) assert (letters, inverses) == ("abcd", "badc") + + +def test_commutator_errors(): + check_commutator_errors(to_word) + check_commutator_errors(to_string) + + +def test_commutator(): + check_commutator(to_word) + check_commutator(to_string) + + +def test_add_commutator_rule_errors(): + check_add_commutator_rule_errors(to_word) + check_add_commutator_rule_errors(to_string) + + +def test_add_commutator_rule(): + check_add_commutator_rule(to_word) + check_add_commutator_rule(to_string) From a052a9a8c413a775b27372728041d62483274843 Mon Sep 17 00:00:00 2001 From: Joseph Edwards Date: Sat, 9 May 2026 22:56:35 +0100 Subject: [PATCH 5/7] detail: accomodate kwargs in cxx wrapper --- etc/check-params.py | 7 +++++-- src/libsemigroups_pybind11/detail/cxx_wrapper.py | 8 ++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/etc/check-params.py b/etc/check-params.py index 4801d38f..74870d91 100644 --- a/etc/check-params.py +++ b/etc/check-params.py @@ -3,6 +3,7 @@ from glob import iglob from bs4 import BeautifulSoup +from bs4.element import Tag BOLD_TEXT = "\033[1m" YELLOW = "\033[93m" @@ -20,7 +21,7 @@ def warn(message): print(YELLOW + f"WARNING: {message}" + END_COLOUR) -def extract_signature(func, func_name) -> tuple[dict[str, str], str]: +def extract_signature(func: Tag, func_name: str) -> tuple[dict[str, str], str]: """Extract the parameters and typehints from the signature of a function This function interrogates the signature of a function and returns: @@ -31,6 +32,8 @@ def extract_signature(func, func_name) -> tuple[dict[str, str], str]: return_typehint = "" sig = func.find("dt", class_="sig sig-object py") for param in sig.find_all("em", class_="sig-param"): + if param.find("span", class_="keyword-only-separator"): + continue param_component = param.find_all("span", class_="n") if len(param_component) == 0 or len(param_component) > 2: warn(f"unexpected element in doc of {func_name}. Skipping . . .") @@ -44,7 +47,7 @@ def extract_signature(func, func_name) -> tuple[dict[str, str], str]: return param_to_typehint, return_typehint -def extract_documented_signature(func, name) -> tuple[dict[str, str], str]: +def extract_documented_signature(func: Tag, name: str) -> tuple[dict[str, str], str]: """Extract the parameters and typehints from the docstring of a function This function interrogates the docstring of a function and returns: diff --git a/src/libsemigroups_pybind11/detail/cxx_wrapper.py b/src/libsemigroups_pybind11/detail/cxx_wrapper.py index 741204c3..31564011 100644 --- a/src/libsemigroups_pybind11/detail/cxx_wrapper.py +++ b/src/libsemigroups_pybind11/detail/cxx_wrapper.py @@ -192,8 +192,12 @@ def wrap_cxx_free_fn(cxx_free_fn: Pybind11Type) -> Callable: returned function. """ - def cxx_free_fn_wrapper(*args): - return to_py(cxx_free_fn(*(to_cxx(x) for x in args))) + def cxx_free_fn_wrapper(*args, **kwargs): + return to_py( + cxx_free_fn( + *(to_cxx(x) for x in args), **{key: to_cxx(value) for key, value in kwargs.items()} + ) + ) update_wrapper(cxx_free_fn_wrapper, cxx_free_fn) return cxx_free_fn_wrapper From 966a41f986b25b0c9e8f1f3acc186e4fa27db8e7 Mon Sep 17 00:00:00 2001 From: Joseph Edwards Date: Sun, 10 May 2026 11:31:25 +0100 Subject: [PATCH 6/7] presentation: add commutator stuff to doc --- .../presentations/present-helpers.rst | 2 + src/present.cpp | 61 ++++++++----------- 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/docs/source/data-structures/presentations/present-helpers.rst b/docs/source/data-structures/presentations/present-helpers.rst index 99f3f3de..d615e9f5 100644 --- a/docs/source/data-structures/presentations/present-helpers.rst +++ b/docs/source/data-structures/presentations/present-helpers.rst @@ -34,6 +34,7 @@ Contents .. autosummary:: :signatures: short + add_commutator_rule add_cyclic_conjugates add_identity_rules add_inverse_rules @@ -43,6 +44,7 @@ Contents are_rules_sorted balance change_alphabet + commutator contains_rule first_unused_letter greedy_reduce_length diff --git a/src/present.cpp b/src/present.cpp index 5b810a97..95bd9bff 100644 --- a/src/present.cpp +++ b/src/present.cpp @@ -574,6 +574,9 @@ alphabet of *p*, and where :math:`z` is the second parameter. py::arg("x"), py::arg("y"), R"pbdoc( +:sig=(p: Presentation, x: Word, y: Word) -> Word: +:only-document-once: + Return the commutator of two words. Returns the word :math:`x^{-1}y^{-1}xy`, after attempting to detect inverses @@ -595,13 +598,6 @@ from the rules in *p*, using :any:`try_detect_inverses`. if *x* or *y* contains a letter not belonging to ``p.alphabet()``, if :any:`try_detect_inverses` throws, or if *x* or *y* contains a letter for which no inverse was detected. - -.. seealso:: - * :any:`try_detect_inverses` and - * :any:`throw_if_letter_not_in_alphabet`. - - - )pbdoc"); m.def( "presentation_commutator", @@ -616,6 +612,9 @@ from the rules in *p*, using :any:`try_detect_inverses`. py::arg("y"), py::arg("inverses"), R"pbdoc( +:sig=(p: Presentation, x: Word, y: Word, inverses: Word) -> Word: +:only-document-once: + Return the commutator of two words. Returns the word :math:`x^{-1}y^{-1}xy`. The letter ``a`` with index ``i`` in @@ -639,10 +638,6 @@ Returns the word :math:`x^{-1}y^{-1}xy`. The letter ``a`` with index ``i`` in :raises LibsemigroupsError: if *inverses* are not valid inverses for ``p.alphabet()``, or if *x* or *y* contains a letter not belonging to ``p.alphabet()``. - -.. seealso:: - * :any:`throw_if_bad_inverses` and - * :any:`throw_if_letter_not_in_alphabet`. )pbdoc"); m.def( "presentation_commutator", @@ -657,6 +652,9 @@ Returns the word :math:`x^{-1}y^{-1}xy`. The letter ``a`` with index ``i`` in py::arg("alphabet"), py::arg("inverses"), R"pbdoc( +:sig=(x: Word, y: Word, alphabet: Word, inverses: Word) -> Word: +:only-document-once: + Return the commutator of two words. Returns the word :math:`x^{-1}y^{-1}xy`. The letter ``a`` with index ``i`` in @@ -682,11 +680,6 @@ Returns the word :math:`x^{-1}y^{-1}xy`. The letter ``a`` with index ``i`` in if *alphabet* contains duplicates, if *inverses* are not valid inverses for *alphabet*, or if *x* or *y* contains a letter not belonging to *alphabet*. - -.. seealso:: - * :any:`throw_if_contains_duplicates`, - * :any:`throw_if_bad_inverses` and - * :any:`throw_if_word_not_over_alphabet`. )pbdoc"); m.def( "presentation_add_commutator_rule", @@ -708,6 +701,9 @@ Returns the word :math:`x^{-1}y^{-1}xy`. The letter ``a`` with index ``i`` in py::arg("id") = static_cast(UNDEFINED), R"pbdoc( +:sig=(p: Presentation, x: Word, y: Word, alphabet: Word, inverses: Word, *, id: Letter = UNDEFINED) -> None: +:only-document-once: + Add a commutator rule. Adds the rule :math:`x^{-1}y^{-1}xy = id` to *p*. The letter ``a`` with index @@ -731,7 +727,9 @@ word. :param inverses: the inverses of the letters in alphabet. :type inverses: :ref:`Word` -:param id: the identity letter, or UNDEFINED for the empty word. +:param id: + the identity letter, or :any:`UNDEFINED` for the empty word. This is a + keyword-only argument. :type id: :ref:`Letter` :raises LibsemigroupsError: @@ -739,12 +737,6 @@ word. belonging to ``p.alphabet()``; if *alphabet* contains duplicates; if *inverses* are not valid inverses for *alphabet*; or if *x* or *y* contains a letter not belonging to *alphabet*. - -.. seealso:: - * :any:`throw_if_contains_duplicates`, - * :any:`throw_if_bad_inverses`, - * :any:`throw_if_word_not_over_alphabet` and - * :any:`throw_if_letter_not_in_alphabet`. )pbdoc"); m.def( "presentation_add_commutator_rule", @@ -763,6 +755,9 @@ word. py::arg("id") = static_cast(UNDEFINED), R"pbdoc( +:sig=(p: Presentation, x: Word, y: Word, inverses: Word, *, id: Letter = UNDEFINED) -> None: +:only-document-once: + Add a commutator rule. Adds the rule :math:`x^{-1}y^{-1}xy = id` to *p*. The letter ``a`` with index @@ -782,7 +777,9 @@ empty word. :param inverses: the inverses of the letters in p.alphabet(). :type inverses: :ref:`Word` -:param id: the identity letter, or UNDEFINED for the empty word. +:param id: + the identity letter, or :any:`UNDEFINED` for the empty word. This is a + keyword-only argument. :type id: :ref:`Letter` @@ -790,10 +787,6 @@ empty word. if *inverses*, *x*, *y*, or *id* contains a letter not belonging to ``p.alphabet()``, or if *inverses* are not valid inverses for ``p.alphabet()``. - -.. seealso:: - * :any:`throw_if_bad_inverses` and - * :any:`throw_if_letter_not_in_alphabet`. )pbdoc"); m.def( "presentation_add_commutator_rule", @@ -810,6 +803,9 @@ empty word. py::arg("id") = static_cast(UNDEFINED), R"pbdoc( +:sig=(p: Presentation, x: Word, y: Word, *, id: Letter = UNDEFINED) -> None: +:only-document-once: + Add a commutator rule. Adds the rule :math:`x^{-1}y^{-1}xy = id` to *p*, after attempting to detect @@ -825,18 +821,15 @@ inverses from the rules in *p*, using :any:`try_detect_inverses`. If :param y: the second word in the commutator. :type y: :ref:`Word` -:param id: the identity letter, or UNDEFINED for the empty word. +:param id: + the identity letter, or :any:`UNDEFINED` for the empty word. This is a + keyword-only argument. :type id: :ref:`Letter` :raises LibsemigroupsError: if *x*, *y*, or *id* contains a letter not belonging to ``p.alphabet()``, if :any:`try_detect_inverses` throws, or if *x* or *y* contains a letter for which no inverse was detected. - -.. seealso:: - * :any:`throw_if_letter_not_in_alphabet`, - * :any:`try_detect_inverses` and - * :any:`throw_if_word_not_over_alphabet`. )pbdoc"); m.def( "presentation_are_rules_sorted", From be58c8845256efe0bc37871c5efe7307e082a87b Mon Sep 17 00:00:00 2001 From: Joseph Edwards Date: Mon, 11 May 2026 11:05:23 +0100 Subject: [PATCH 7/7] disable some linter warnings --- tests/test_present.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_present.py b/tests/test_present.py index f6936a35..87bf262c 100644 --- a/tests/test_present.py +++ b/tests/test_present.py @@ -194,7 +194,7 @@ def check_add_identity_rules(W): ] -def check_commutator_errors(W): +def check_commutator_errors(W): # pylint: disable=too-many-statements # Alphabet specified, inverses specified # Words not over the alphabet @@ -320,7 +320,7 @@ def check_commutator(W): assert presentation.commutator(p, W([0, 0]), W([2])) == W([2, 2, 0, 0, 0, 2]) -def check_add_commutator_rule_errors(W): +def check_add_commutator_rule_errors(W): # pylint: disable=too-many-statements p = Presentation(W([])) with pytest.raises(LibsemigroupsError):