From 23ebfded89dcf8d3314dfb649c70bb7553993cfd Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Mon, 16 Feb 2026 11:20:08 +0000 Subject: [PATCH 1/4] todd-coxeter: update for changes in libsemigroups --- .../todd-coxeter/class/modifiers.rst | 39 +- etc/catch-cpp-to-pytest-f1.vim | 18 + ...pp-to-pytest.vim => doxy-to-sphinx-f2.vim} | 29 +- src/todd-coxeter-impl.cpp | 11 +- src/todd-coxeter.cpp | 337 +++++++++++++++++- tests/test_todd_coxeter.py | 84 ++++- 6 files changed, 487 insertions(+), 31 deletions(-) create mode 100644 etc/catch-cpp-to-pytest-f1.vim rename etc/{catch-cpp-to-pytest.vim => doxy-to-sphinx-f2.vim} (50%) diff --git a/docs/source/main-algorithms/todd-coxeter/class/modifiers.rst b/docs/source/main-algorithms/todd-coxeter/class/modifiers.rst index 17a22f2b0..264bb1ceb 100644 --- a/docs/source/main-algorithms/todd-coxeter/class/modifiers.rst +++ b/docs/source/main-algorithms/todd-coxeter/class/modifiers.rst @@ -1,5 +1,5 @@ .. - Copyright (c) 2024 J. D. Mitchell + Copyright (c) 2024-2026 J. D. Mitchell Distributed under the terms of the GPL license version 3. @@ -15,8 +15,45 @@ that can be used to modify the state of a :any:`ToddCoxeter` instance. In other words, for modifying the :any:`WordGraph` that is the output of the algorithm in a way that preserves it up to isomorphism. +Contents +-------- + +.. autosummary:: + :signatures: short + + ToddCoxeter.perform_lookahead + ToddCoxeter.perform_lookahead_for + ToddCoxeter.perform_lookahead_until + ToddCoxeter.perform_lookbehind + ToddCoxeter.perform_lookbehind_no_checks + ToddCoxeter.perform_lookbehind_for + ToddCoxeter.perform_lookbehind_for_no_checks + ToddCoxeter.perform_lookbehind_until + ToddCoxeter.perform_lookbehind_until_no_checks + ToddCoxeter.shrink_to_fit + ToddCoxeter.standardize + +Full API +-------- + .. automethod:: ToddCoxeter.perform_lookahead +.. automethod:: ToddCoxeter.perform_lookahead_for + +.. automethod:: ToddCoxeter.perform_lookahead_until + +.. automethod:: ToddCoxeter.perform_lookbehind + +.. automethod:: ToddCoxeter.perform_lookbehind_no_checks + +.. automethod:: ToddCoxeter.perform_lookbehind_for + +.. automethod:: ToddCoxeter.perform_lookbehind_for_no_checks + +.. automethod:: ToddCoxeter.perform_lookbehind_until + +.. automethod:: ToddCoxeter.perform_lookbehind_until_no_checks + .. automethod:: ToddCoxeter.shrink_to_fit .. automethod:: ToddCoxeter.standardize diff --git a/etc/catch-cpp-to-pytest-f1.vim b/etc/catch-cpp-to-pytest-f1.vim new file mode 100644 index 000000000..04421b24a --- /dev/null +++ b/etc/catch-cpp-to-pytest-f1.vim @@ -0,0 +1,18 @@ +function! CatchCPPToPytest() + silent '<,'>s/{/[/ge + silent '<,'>s/}/]/ge + silent '<,'>s/::/./ge + silent '<,'>s/true/True/ge + silent '<,'>s/false/False/ge + silent '<,'>s/;//ge + silent '<,'>s/REQUIRE(\([^)]*\))/assert \1/ge + silent '<,'>s/<[^>]*>//ge + silent '<,'>s/REQUIRE_THROWS_AS(\([^,]\+\),\s\+LibsemigroupsException)/with pytest.raises(RuntimeError):\r assert \1/ge + silent '<,'>s/\(\d\)\(\d\+_w\)/\1, \2/ge + silent '<,'>s/\<\(\d\)_w/\1]/ge + silent '<,'>s/!/not /ge + silent '<,'>s/\/\/.*$//ge +endfunction + +map! :call CatchCPPToPytest()i +map :call CatchCPPToPytest() diff --git a/etc/catch-cpp-to-pytest.vim b/etc/doxy-to-sphinx-f2.vim similarity index 50% rename from etc/catch-cpp-to-pytest.vim rename to etc/doxy-to-sphinx-f2.vim index c3f592ae8..82392f84b 100644 --- a/etc/catch-cpp-to-pytest.vim +++ b/etc/doxy-to-sphinx-f2.vim @@ -1,29 +1,12 @@ -function! CatchCPPToPytest() - silent '<,'>s/{/[/ge - silent '<,'>s/}/]/ge - silent '<,'>s/::/./ge - silent '<,'>s/true/True/ge - silent '<,'>s/false/False/ge - silent '<,'>s/;//ge - silent '<,'>s/REQUIRE(\([^)]*\))/assert \1/ge - silent '<,'>s/<[^>]*>//ge - silent '<,'>s/REQUIRE_THROWS_AS(\([^,]\+\),\s\+LibsemigroupsException)/with pytest.raises(RuntimeError):\r assert \1/ge - silent '<,'>s/\(\d\)\(\d\+_w\)/\1, \2/ge - silent '<,'>s/\<\(\d\)_w/\1]/ge - silent '<,'>s/!/not /ge - silent '<,'>s/\/\/.*$//ge -endfunction - function! DoxyToSphinx() silent '<,'>s/\/\/!//ge silent '<,'>s/^.*\\tparam.*$//ge - silent '<,'>s/\\param\s\+\(\w\)\+\(.*\)$/:param \1: \2\r :type \1: ??/ge + silent '<,'>s/\\param\s\+\(\w\+\)/:param \1:/ge silent '<,'>s/\\returns/:returns:/ge silent '<,'>s/\\throws LibsemigroupsException/:raises RuntimeError:/ge silent '<,'>s/`\{-1}/``/ge silent '<,'>s/`\{4}/``/ge - silent '<,'>s/\\p\s\+\(\w\+\)/``\1``/ge - silent '<,'>s/\\ref\s\+\(\w\+\)/:py:any:`\1`/ge + silent '<,'>s/\\p\s\+\(\w\+\)/*\1*/ge silent '<,'>s/\\c\s\+\(\w\+\)/``\1``/ge silent '<,'>s/``true``/``True``/ge silent '<,'>s/``false``/``False``/ge @@ -33,9 +16,13 @@ function! DoxyToSphinx() silent '<,'>s/\\f\$\(.\{-}\)\\f\$/:math:`\1`/ge silent '<,'>s/:math:``\(.\{-}\)``/:math:`\1`/ge silent '<,'>s/``\(.\{-}\)``_/`\1`_/ge + silent '<,'>s/\\ref_knuth_bendix/:any:`KnuthBendix`/ge + silent '<,'>s/\\brief//ge + silent '<,'>s/\\ref\s\+\([a-zA-z:]\+\)/:any:`\1`/ge + silent '<,'>s/:any:``\(.\{-}\)``/:any:`\1`/ge + silent '<,'>s/::/./ge + silent '<,'>s/^\s*//ge endfunction -map! :call CatchCPPToPytest()i -map :call CatchCPPToPytest() map! :call DoxyToSphinx()i map :call DoxyToSphinx() diff --git a/src/todd-coxeter-impl.cpp b/src/todd-coxeter-impl.cpp index eb4fcc7e6..0854a7a42 100644 --- a/src/todd-coxeter-impl.cpp +++ b/src/todd-coxeter-impl.cpp @@ -1400,10 +1400,13 @@ returned by this function may not be compatible with the relations of // Modifiers //////////////////////////////////////////////////////////////////////// - thing.def("perform_lookahead", - &ToddCoxeterImpl_::perform_lookahead, - py::arg("stop_early"), - R"pbdoc( + thing.def( + "perform_lookahead", + [](ToddCoxeterImpl_& self, bool stop_early) { + self.perform_lookahead(stop_early); + }, + py::arg("stop_early"), + R"pbdoc( :sig=(self: ToddCoxeter, stop_early: bool) -> None: Perform a lookahead. diff --git a/src/todd-coxeter.cpp b/src/todd-coxeter.cpp index 3a119575f..e7e46e4da 100644 --- a/src/todd-coxeter.cpp +++ b/src/todd-coxeter.cpp @@ -22,6 +22,7 @@ // pybind11.... #include +#include #include #include @@ -406,6 +407,337 @@ enumeration of ``tc``.)pbdoc", .only_document_once = true, .raises = raises, .var = "tc"}); + thing.def( + "perform_lookahead", + [](ToddCoxeter_& self) { return self.perform_lookahead(); }, + R"pbdoc( +:sig=(self: ToddCoxeter) -> ToddCoxeter: + +Perform a lookahead. + +This function can be used to explicitly perform a lookahead. The +style and extent of this lookahead are controlled by the settings +:any:`ToddCoxeter.lookahead_style` and :any:`ToddCoxeter.lookahead_extent`. + +:returns: *self* +:rtype: ToddCoxeter + +.. seealso:: + :any:`ToddCoxeter.perform_lookahead_for` and + :any:`ToddCoxeter.perform_lookahead_until`. +)pbdoc"); + + thing.def( + "perform_lookahead_for", + [](ToddCoxeter_& self, std::chrono::nanoseconds t) { + return self.perform_lookahead_for(t); + }, + py::arg("t"), + R"pbdoc( +:sig=(self: ToddCoxeter, t: datetime.timedelta) -> ToddCoxeter: + +Perform a lookahead for a specified amount of time. + +This function runs a lookahead for approximately the amount of time +indicated by *t*, or until the lookahead is complete whichever +happens first. + +:param t: the time to run for. +:type t: datetime.timedelta + +:returns: *self* +:rtype: ToddCoxeter +)pbdoc"); + + thing.def( + "perform_lookahead_until", + [](ToddCoxeter_& self, std::function const& pred) { + return self.perform_lookahead_until(pred); + }, + py::arg("pred"), + R"pbdoc( +:sig=(self: ToddCoxeter, pred: collections.abc.Callable[[], bool]) -> ToddCoxeter: + +Perform a lookahead until a nullary predicate returns ``True``. + +This function runs a lookahead until the nullary predicate *pred* returns +``True``, or until the lookahead is complete whichever happens first. + +:param pred: the nullary predicate. +:type pred: collections.abc.Callable[[], bool] + +:returns: *self* +:rtype: ToddCoxeter +)pbdoc"); + + thing.def( + "perform_lookbehind", + [](ToddCoxeter_& self) { return self.perform_lookbehind(); }, + R"pbdoc( +:sig=(self: ToddCoxeter) -> ToddCoxeter: + +Perform a lookbehind. + +This function performs a "lookbehind" which is defined as follows. For every +node ``n`` in the so-far computed :any:`WordGraph` (obtained from +:any:`ToddCoxeter.current_word_graph`) we use the current word graph to +rewrite the current short-lex least path from the initial node to ``n``. If +this rewritten word is not equal to the original word, and it also labels a +path from the initial node in the current word graph to a node ``m``, then +``m`` and ``n`` represent the same congruence class. Thus we may collapse +``m`` and ``n`` (i.e. quotient the word graph by the least congruence +containing the pair ``m`` and ``n``). + +The intended use case for this function is when you have a large word +graph in a partially enumerated :any:`ToddCoxeter` instance, and you +would like to minimise this word graph as far as possible. + +For example, if we take the following monoid presentation of B. H. +Neumann for the trivial group: + +.. code-block:: python + + p = Presentation("abcdef") + p.contains_empty_word(True) + presentation.add_inverse_rules(p, "defabc") + presentation.add_rule(p, "bbdeaecbffdbaeeccefbccefb", "") + presentation.add_rule(p, "ccefbfacddecbffaafdcaafdc", "") + presentation.add_rule(p, "aafdcdbaeefacddbbdeabbdea", "") + tc = ToddCoxeter(congruence_kind.twosided, p) + +Then running *tc* will simply grow the underlying word graph until +your computer runs out of memory. The authors of ``libsemigroups`` were +not able to find any combination of the many settings for +:any:`ToddCoxeter` where running *tc* returned an answer. We also tried +with GAP and ACE but neither of these seemed able to return an answer +either. But doing the following: + +.. code-block:: python + + tc.run_until(lambda: tc.number_of_nodes_active() >= 12_000_000) + tc.perform_lookahead(); + tc.perform_lookbehind(); + + tc.number_of_classes() # returns 1 + +returns the correct answer in about 5 seconds (on a 2024 Macbook Pro M4 +Pro). + +:returns: *self* +:rtype: ToddCoxeter + +:raises LibsemigroupsError: + if *self* is a one-sided congruence and has any generating pairs (because + in this case this function does nothing but still might take some time to + run). +)pbdoc"); + + thing.def( + "perform_lookbehind_no_checks", + [](ToddCoxeter_& self, + std::function const& collapser) { + auto wrap = [&collapser](auto d_it, auto first, auto last) { + Word copy(first, last); + // Shame to do so much copying here but couldn't figure out how to + // pass a word by reference easily in python + copy = collapser(copy); + std::copy(copy.begin(), copy.end(), d_it); + }; + return self.perform_lookbehind_no_checks(wrap); + }, + R"pbdoc( +:sig=(self: ToddCoxeter, collapser: collections.abc.Callable[[Word], Word]) -> ToddCoxeter: + +Perform a lookbehind using a function to decide whether or not +to collapse nodes. + +This function perform a lookbehind using the function *collapser* to decide +whether or not to collapse nodes. For example, it might be the case that +*collapser* uses a :any:`KnuthBendix` instance to determine whether or +not nodes in the graph represent the same class of the congruence. More +specifically, the shortlex least path from the initial node to every node +``n`` is rewritten using *collapser*, and if the rewritten word labels a +path in the graph to a node ``m``, then it is assumed that ``m`` and ``n`` +represent the same class of the congruence, and they are marked for +collapsing. For example, :any:`perform_lookbehind` calls +:any:`perform_lookbehind_no_checks` where *collapser* is the member +function :any:`ToddCoxeter.reduce_no_run`. + +:param collapser: + a function taking a ``str`` or ``list[int]`` (depending on the type used by + *self* for words) and which returns a word equivalent to the input word in + the congruence represented by *self*. +:type collapser: collections.abc.Callable[[Word], Word]) + +:returns: *self* +:rtype: ToddCoxeter + +:raises LibsemigroupsError: if *self* is a one-sided congruence and + has any generating pairs (because in this case :any:`perform_lookbehind` + does nothing but still might take some time to run). + +.. warning. + No checks are performed on the argument *collapser* to ensure that the word + graph produced by using it to collapse nodes is valid. It is the + responsibility of the caller to ensure that this is valid. + +.. doctest:: + + >>> from libsemigroups_pybind11 import (presentation, Presentation, + ... ToddCoxeter, congruence_kind) + >>> from datetime import timedelta + >>> p = Presentation("abcdef") + >>> p.contains_empty_word(True) + + >>> presentation.add_inverse_rules(p, "defabc") + >>> presentation.add_rule(p, "bbdeaecbffdbaeeccefbccefb", "") + >>> presentation.add_rule(p, "ccefbfacddecbffaafdcaafdc", "") + >>> presentation.add_rule(p, "aafdcdbaeefacddbbdeabbdea", "") + >>> tc = ToddCoxeter(congruence_kind.twosided, p) + >>> tc.run_for(timedelta(seconds=0.01)) + >>> tc.perform_lookbehind_no_checks(lambda w: "") # just an example, not good! + <2-sided ToddCoxeter over with 0 gen. pairs + 1 node> + >>> tc.number_of_classes() + 1 +)pbdoc"); + + thing.def( + "perform_lookbehind_for", + [](ToddCoxeter_& self, std::chrono::nanoseconds t) { + return self.perform_lookbehind_for(t); + }, + py::arg("t"), + R"pbdoc( +:sig=(self: ToddCoxeter, t: datetime.timedelta) -> ToddCoxeter: + +Perform a lookbehind for a specified amount of time. + +This function runs a lookbehind for approximately the amount of time +indicated by *t*, or until the lookbehind is complete whichever +happens first. + +:param t: the time to run for. +:type t: datetime.timedelta + +:returns: *self* +:rtype: ToddCoxeter + +:raises LibsemigroupsError: if *self* is a one-sided congruence and + has any generating pairs (because in this case this function + does nothing but still might take some time to run). +)pbdoc"); + + thing.def( + "perform_lookbehind_for_no_checks", + [](ToddCoxeter_& self, + std::chrono::nanoseconds t, + std::function const& collapser) { + auto wrap = [&collapser](auto d_it, auto first, auto last) { + Word copy(first, last); + // Shame to do so much copying here but couldn't figure out how to + // pass a word by reference easily in python + copy = collapser(copy); + std::copy(copy.begin(), copy.end(), d_it); + }; + return self.perform_lookbehind_for_no_checks(t, wrap); + }, + py::arg("t"), + py::arg("collapser"), + R"pbdoc( +:sig=(self: ToddCoxeter, t: datetime.timedelta, collapser: collections.abc.Callable[[Word], Word]) -> ToddCoxeter: + +Perform a lookbehind for a specified amount of time with a +collapser. + +This function runs a lookbehind using *collapser* for approximately the amount +of time indicated by *t*, or until the lookbehind is complete whichever +happens first. See :any:`perform_lookbehind_no_checks` for more details. + +:param t: the time to run for. +:type t: datetime.timedelta + +:param collapser: + a function taking a ``str`` or ``list[int]`` (depending on the type used by + *self* for words) and which returns a word equivalent to the input word in + the congruence represented by *self*. +:type collapser: collections.abc.Callable[[Word], Word]) + +:returns: A reference to ``*this``. + +:returns: *self* +:rtype: ToddCoxeter + +:raises LibsemigroupsError: if *self* is a one-sided congruence and + has any generating pairs (because in this case this function + does nothing but still might take some time to run). +)pbdoc"); + + thing.def( + "perform_lookbehind_until", + [](ToddCoxeter_& self, std::function const& pred) { + return self.perform_lookbehind_until(pred); + }, + py::arg("pred"), + R"pbdoc( +:sig=(self: ToddCoxeter, pred: collections.abc.Callable[[], bool]) -> ToddCoxeter: + +Perform a lookbehind until a nullary predicate returns ``True``. + +This function runs a lookbehind until the nullary predicate *pred* returns +``True``, or until the lookbehind is complete whichever happens first. + +:param pred: the nullary predicate. +:type pred: collections.abc.Callable[[], bool] + +:returns: *self* +:rtype: ToddCoxeter + +:raises LibsemigroupsError: if *self* is a one-sided congruence and + has any generating pairs (because in this case this function + does nothing but still might take some time to run). +)pbdoc"); + + thing.def( + "perform_lookbehind_until_no_checks", + [](ToddCoxeter_& self, + std::function const& pred, + std::function const& collapser) { + auto wrap = [&collapser](auto d_it, auto first, auto last) { + Word copy(first, last); + // Shame to do so much copying here but couldn't figure out how to + // pass a word by reference easily in python + copy = collapser(copy); + std::copy(copy.begin(), copy.end(), d_it); + }; + return self.perform_lookbehind_until_no_checks(pred, wrap); + }, + py::arg("pred"), + py::arg("collapser"), + R"pbdoc( +:sig=(self: ToddCoxeter, pred: collections.abc.Callable[[], bool], collapser: collections.abc.Callable[[Word], Word]) -> ToddCoxeter: + +Perform a lookbehind until a nullary predicate returns ``True``. + +This function runs a lookbehind using *collapser* until the nullary +predicate *pred* returns ``True``, or until the lookbehind is complete +whichever happens first. + +:param pred: the nullary predicate. +:type pred: collections.abc.Callable[[], bool] + +:param collapser: + a function taking a ``str`` or ``list[int]`` (depending on the type used by + *self* for words) and which returns a word equivalent to the input word in + the congruence represented by *self*. +:type collapser: collections.abc.Callable[[Word], Word]) + +:returns: *self* +:rtype: ToddCoxeter + +:raises LibsemigroupsError: if *self* is a one-sided congruence and + has any generating pairs (because in this case this function + does nothing but still might take some time to run). +)pbdoc"); //////////////////////////////////////////////////////////////////////// // Helper functions - specific to ToddCoxeter @@ -600,7 +932,7 @@ Neumann for the trivial group: .. code-block:: python p = Presentation("abcdef") - p.contains_empty_word(true) + p.contains_empty_word(True) presentation.add_inverse_rules(p, "defabc") presentation.add_rule(p, "bbdeaecbffdbaeeccefbccefb", "") presentation.add_rule(p, "ccefbfacddecbffaafdcaafdc", "") @@ -616,8 +948,7 @@ either. But doing the following: .. code-block:: python - tc.lookahead_extent(options.lookahead_extent.full) - .lookahead_style(options.lookahead_style.felsch) + tc.lookahead_extent(tc.options.lookahead_extent.full).lookahead_style(tc.options.lookahead_style.felsch) tc.run_for(timedelta(seconds=1)) tc.perform_lookahead(True) diff --git a/tests/test_todd_coxeter.py b/tests/test_todd_coxeter.py index 05becaf63..f504c7dca 100644 --- a/tests/test_todd_coxeter.py +++ b/tests/test_todd_coxeter.py @@ -8,6 +8,7 @@ # pylint: disable=missing-function-docstring, invalid-name +import time from datetime import timedelta import pytest @@ -28,6 +29,7 @@ tril, word_graph, ) +from libsemigroups_pybind11.presentation import examples from .cong_common import check_congruence_common_return_policy @@ -368,7 +370,7 @@ def test_current_word_of(): assert tree is tc.spanning_tree() tc.init() assert tree is tc.current_spanning_tree() - assert tree.number_of_nodes() == 0 + assert tree.number_of_nodes() == 1 assert wg is tc.word_graph() assert wg.number_of_nodes() == 1 @@ -378,7 +380,7 @@ def test_todd_coxeter_return_policy(): tc = check_congruence_common_return_policy(ToddCoxeter) # Initializers assert tc.init(congruence_kind.twosided, tc) is tc - assert tc.init(congruence_kind.twosided, tc.current_word_graph()) is tc + assert tc.init(congruence_kind.twosided, tc.current_word_graph().copy()) is tc # Options assert tc.def_max(10) is tc @@ -413,3 +415,81 @@ def test_todd_coxeter_return_policy(): assert tc.spanning_tree() is tc.spanning_tree() assert tc.word_graph() is tc.word_graph() + + +def test_todd_coxeter_perform_lookahead(): + p = examples.full_transformation_monoid_Aiz58(10) + tc = ToddCoxeter(congruence_kind.twosided, p) + tc.perform_lookahead() + tc.run_for(timedelta(seconds=0.01)) + num_nodes = tc.number_of_nodes_active() + tc.perform_lookahead() + assert tc.number_of_nodes_active() < num_nodes + + +def test_todd_coxeter_perform_lookahead_for(): + p = examples.full_transformation_monoid_Aiz58(10) + tc = ToddCoxeter(congruence_kind.twosided, p) + tc.perform_lookahead() + tc.run_for(timedelta(seconds=0.01)) + num_nodes = tc.number_of_nodes_active() + start_time = time.time() + tc.perform_lookahead_for(timedelta(seconds=0.1)) + assert time.time() - start_time <= 0.2 + assert tc.number_of_nodes_active() < num_nodes + + +def test_todd_coxeter_perform_lookahead_until(): + p = examples.full_transformation_monoid_Aiz58(10) + tc = ToddCoxeter(congruence_kind.twosided, p) + tc.perform_lookahead() + tc.run_for(timedelta(seconds=0.01)) + num_nodes = tc.number_of_nodes_active() + tc.perform_lookahead_until(lambda: tc.number_of_nodes_active() < num_nodes) + assert tc.number_of_nodes_active() < num_nodes + + +def test_todd_coxeter_perform_lookbehind_for(): + p = examples.full_transformation_monoid_Aiz58(10) + tc = ToddCoxeter(congruence_kind.twosided, p) + tc.perform_lookbehind() + tc.run_for(timedelta(seconds=0.01)) + num_nodes = tc.number_of_nodes_active() + start_time = time.time() + tc.perform_lookbehind_for(timedelta(seconds=0.1)) + assert time.time() - start_time <= 0.2 + assert tc.number_of_nodes_active() < num_nodes + + +def test_todd_coxeter_perform_lookbehind_until(): + p = examples.full_transformation_monoid_Aiz58(10) + tc = ToddCoxeter(congruence_kind.twosided, p) + tc.perform_lookbehind() + tc.run_for(timedelta(seconds=0.01)) + num_nodes = tc.number_of_nodes_active() + tc.perform_lookbehind_until(lambda: tc.number_of_nodes_active() < num_nodes) + assert tc.number_of_nodes_active() < num_nodes + + +def test_todd_coxeter_perform_lookbehind_for_no_checks(): + p = examples.full_transformation_monoid_Aiz58(10) + tc = ToddCoxeter(congruence_kind.twosided, p) + tc.perform_lookbehind() + tc.run_for(timedelta(seconds=0.01)) + num_nodes = tc.number_of_nodes_active() + start_time = time.time() + tc.perform_lookbehind_for_no_checks(timedelta(seconds=0.1), lambda w: []) + assert time.time() - start_time <= 0.2 + assert tc.number_of_nodes_active() < num_nodes + + +def test_todd_coxeter_perform_lookbehind_until_no_checks(): + p = examples.full_transformation_monoid_Aiz58(10) + tc = ToddCoxeter(congruence_kind.twosided, p) + tc.perform_lookbehind() + tc.run_for(timedelta(seconds=0.01)) + num_nodes = tc.number_of_nodes_active() + tc.perform_lookbehind_until_no_checks( + lambda: tc.number_of_nodes_active() < num_nodes, lambda w: [] + ) + assert tc.number_of_nodes_active() < num_nodes From b323671f579b138b6804d2255da70dca0be5264c Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Mon, 23 Feb 2026 12:51:26 +0000 Subject: [PATCH 2/4] bmat8: fix doc typos --- src/bmat8.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/bmat8.cpp b/src/bmat8.cpp index 93023b018..270ac1a7a 100644 --- a/src/bmat8.cpp +++ b/src/bmat8.cpp @@ -195,13 +195,13 @@ lists in *rows*. :raises LibsemigroupsError: if the rows of *rows* are not all of the same length. :complexity: Constant.)pbdoc"); - thing.def( - "degree", [](BMat8 const& self) { return 8; }, R"pbdoc( + + thing.def("degree", [](BMat8 const& self) { return 8; }, R"pbdoc( Returns the degree of *self*. This function always returns ``8``. -:returns: The degree of the matrisx, ``8``. +:returns: The degree of the matrix, ``8``. :rtype: int )pbdoc"); @@ -223,7 +223,7 @@ Returns the integer representation of a :any:`BMat8`. Returns a non-negative integer obtained by interpreting an 8 x 8 :any:`BMat8` as a sequence of 64 bits (reading rows left to right, from top to bottom) and -then realising this sequence as an unsigned int. +then realising this sequence as an integer. :returns: The integer value of the matrix. From 77b32b70162b9796e1050e6e80329054169f406e Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Mon, 23 Feb 2026 18:10:23 +0000 Subject: [PATCH 3/4] Clang-format --- src/bmat8.cpp | 3 ++- src/cong.cpp | 2 +- src/froidure-pin.cpp | 2 +- src/hpcombi.cpp | 2 +- src/konieczny.cpp | 2 +- src/main.cpp | 4 ++-- src/present.cpp | 2 +- src/schreier-sims.cpp | 2 +- src/todd-coxeter.cpp | 2 +- 9 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/bmat8.cpp b/src/bmat8.cpp index 270ac1a7a..bccd149b7 100644 --- a/src/bmat8.cpp +++ b/src/bmat8.cpp @@ -196,7 +196,8 @@ lists in *rows*. :complexity: Constant.)pbdoc"); - thing.def("degree", [](BMat8 const& self) { return 8; }, R"pbdoc( + thing.def( + "degree", [](BMat8 const& self) { return 8; }, R"pbdoc( Returns the degree of *self*. This function always returns ``8``. diff --git a/src/cong.cpp b/src/cong.cpp index 94c75722b..7a439b690 100644 --- a/src/cong.cpp +++ b/src/cong.cpp @@ -250,7 +250,7 @@ obviously infinite; ``False`` is returned if it is not. congruence has infinitely many classes. )pbdoc"); } // bind_cong - } // namespace + } // namespace void init_cong(py::module& m) { bind_cong(m, "CongruenceWord"); diff --git a/src/froidure-pin.cpp b/src/froidure-pin.cpp index ea427bd0a..c31e41b05 100644 --- a/src/froidure-pin.cpp +++ b/src/froidure-pin.cpp @@ -1269,7 +1269,7 @@ This function returns the element of *fp* obtained by evaluating *w*. }); } } // bind_froidure_pin_stateful - } // namespace + } // namespace void init_froidure_pin(py::module& m) { bind_froidure_pin_stateless>(m, "Transf1"); diff --git a/src/hpcombi.cpp b/src/hpcombi.cpp index 30708cc0e..7245cc02c 100644 --- a/src/hpcombi.cpp +++ b/src/hpcombi.cpp @@ -2406,7 +2406,7 @@ This function returns a newly constructed :any:`PPerm16` with the same image as True )pbdoc"); } // init_hpcombi_pperm16 - } // namespace + } // namespace } // namespace HPCombi namespace libsemigroups { diff --git a/src/konieczny.cpp b/src/konieczny.cpp index d87788135..801b92d5a 100644 --- a/src/konieczny.cpp +++ b/src/konieczny.cpp @@ -740,7 +740,7 @@ not already known. int )pbdoc"); } // bind_konieczny - } // namespace + } // namespace void init_konieczny(py::module& m) { bind_konieczny(m, "BMat8"); diff --git a/src/main.cpp b/src/main.cpp index caba008a7..fe397befe 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -36,13 +36,13 @@ namespace libsemigroups { #ifdef VERSION_INFO m.attr("__version__") = VERSION_INFO; #else - m.attr("__version__") = "dev"; + m.attr("__version__") = "dev"; #endif #ifdef LIBSEMIGROUPS_EIGEN_ENABLED m.attr("LIBSEMIGROUPS_EIGEN_ENABLED") = static_cast(LIBSEMIGROUPS_EIGEN_ENABLED); #else - m.attr("LIBSEMIGROUPS_EIGEN_ENABLED") = false; + m.attr("LIBSEMIGROUPS_EIGEN_ENABLED") = false; #endif #ifdef LIBSEMIGROUPS_HPCOMBI_ENABLED diff --git a/src/present.cpp b/src/present.cpp index 50c89e92e..66e09ae2b 100644 --- a/src/present.cpp +++ b/src/present.cpp @@ -1655,7 +1655,7 @@ defined in the alphabet, and that the inverses act as semigroup inverses. * :any:`presentation.throw_if_bad_inverses` )pbdoc"); } // bind_inverse_present - } // namespace + } // namespace void init_present(py::module& m) { bind_present(m, "PresentationWord"); diff --git a/src/schreier-sims.cpp b/src/schreier-sims.cpp index 2cc5b5edf..20dc8464f 100644 --- a/src/schreier-sims.cpp +++ b/src/schreier-sims.cpp @@ -516,7 +516,7 @@ corresponding to the intersection of *x* and *y*. :raises LibsemigroupsError: if *result* is not empty. )pbdoc"); } // bind_schreier_sims - } // namespace + } // namespace void init_schreier_sims(py::module& m) { // One call to bind is required per list of types diff --git a/src/todd-coxeter.cpp b/src/todd-coxeter.cpp index e7e46e4da..b7ce76e84 100644 --- a/src/todd-coxeter.cpp +++ b/src/todd-coxeter.cpp @@ -964,7 +964,7 @@ Pro). :param tc: the :any:`ToddCoxeter` instance. :type tc: ToddCoxeter)pbdoc"); } // bind_todd_coxeter - } // namespace + } // namespace void init_todd_coxeter(py::module& m) { bind_todd_coxeter(m, "ToddCoxeterWord"); From ee5bc098888f57a0d5bc4b80e0f5c80dcb8a6744 Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Mon, 23 Feb 2026 18:11:03 +0000 Subject: [PATCH 4/4] Add clang-format to format make target --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 49d26eca2..726e94dfb 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,7 @@ format: # See https://github.com/astral-sh/ruff/issues/8232 for updates. ruff check --select I --fix-only ruff format + find src -name "*.*pp" -type f -exec clang-format-15 -i --verbose {} + check: doctest pytest -vv tests/test_*.py