From 7bc1e89ad445449592e9f02af64e8cfa0d1d7ede Mon Sep 17 00:00:00 2001 From: James Mitchell Date: Mon, 16 Feb 2026 11:20:08 +0000 Subject: [PATCH] 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..9dda78ede 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.01)) + assert 0.02 >= time.time() - start_time >= 0.01 + 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.01)) + assert 0.02 >= time.time() - start_time >= 0.01 + 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.01), lambda w: []) + assert time.time() - start_time >= 0.01 + 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