diff --git a/deps/src/CMakeLists.txt b/deps/src/CMakeLists.txt index 20b51a5..4f33dda 100644 --- a/deps/src/CMakeLists.txt +++ b/deps/src/CMakeLists.txt @@ -59,6 +59,7 @@ add_library(libsemigroups_julia SHARED presentation.cpp presentation-examples.cpp knuth-bendix.cpp + todd-coxeter.cpp ) # Include directories @@ -84,6 +85,10 @@ target_link_libraries(libsemigroups_julia target_compile_options(libsemigroups_julia PRIVATE "-I${LIBSEMIGROUPS_INCLUDEDIR}" -fno-char8_t # Work around C++20 char8_t issues with fmt in libsemigroups + # Silence noisy upstream warnings from libsemigroups iterator templates + # under C++20's reversed-operator rules. The semantics are unambiguous; + # only Clang/AppleClang emit this warning. + $<$:-Wno-ambiguous-reversed-operator> ) # Set RPATH to find libsemigroups at runtime diff --git a/deps/src/cong-common.cpp b/deps/src/cong-common.cpp index 2352da4..1c6129f 100644 --- a/deps/src/cong-common.cpp +++ b/deps/src/cong-common.cpp @@ -18,18 +18,10 @@ // CRITICAL: libsemigroups_julia.hpp MUST be included first (fmt consteval fix) #include "libsemigroups_julia.hpp" -#include #include -#include -#include #include -#include - -#include -#include #include -#include namespace jlcxx { template <> @@ -44,123 +36,17 @@ namespace jlcxx { namespace libsemigroups_julia { - namespace { - template - void define_cong_common_word_helpers(jl::Module& m) { - using Word = typename Thing::native_word_type; - - // reduce (triggers full enumeration) - m.method("cong_common_reduce", - [](Thing& self, jlcxx::ArrayRef w) -> Word { - Word input(w.begin(), w.end()); - return libsemigroups::congruence_common::reduce(self, input); - }); - - // reduce_no_run (no enumeration) - m.method("cong_common_reduce_no_run", - [](Thing const& self, jlcxx::ArrayRef w) -> Word { - Word input(w.begin(), w.end()); - return libsemigroups::congruence_common::reduce_no_run(self, - input); - }); - - // contains (triggers full enumeration) - m.method("cong_common_contains", - [](Thing& self, - jlcxx::ArrayRef u, - jlcxx::ArrayRef v) -> bool { - Word uw(u.begin(), u.end()); - Word vw(v.begin(), v.end()); - return libsemigroups::congruence_common::contains( - self, uw, vw); - }); - - // currently_contains (no enumeration, returns tril) - m.method("cong_common_currently_contains", - [](Thing const& self, - jlcxx::ArrayRef u, - jlcxx::ArrayRef v) -> libsemigroups::tril { - Word uw(u.begin(), u.end()); - Word vw(v.begin(), v.end()); - return libsemigroups::congruence_common::currently_contains( - self, uw, vw); - }); - - // add_generating_pair! - m.method("cong_common_add_generating_pair!", - [](Thing& self, - jlcxx::ArrayRef u, - jlcxx::ArrayRef v) { - Word uw(u.begin(), u.end()); - Word vw(v.begin(), v.end()); - libsemigroups::congruence_common::add_generating_pair( - self, uw, vw); - }); - - m.method("cong_common_partition", - [](Thing& self, jlcxx::ArrayRef words) - -> std::vector> { - std::vector input; - input.reserve(words.size()); - for (jl_value_t* word_value : words) { - auto word = jlcxx::ArrayRef( - reinterpret_cast(word_value)); - input.emplace_back(word.begin(), word.end()); - } - return libsemigroups::congruence_common::partition( - self, input.begin(), input.end()); - }); - } - - template - void define_cong_common_normal_forms(jl::Module& m) { - using Word = typename Thing::native_word_type; - - // normal_forms() returns an rx-style range; use - // .at_end()/.get()/.next(). - m.method( - "cong_common_normal_forms", [](Thing& self) -> std::vector { - std::vector result; - auto range = libsemigroups::congruence_common::normal_forms(self); - while (!range.at_end()) { - result.push_back(range.get()); - range.next(); - } - return result; - }); - } - - template - void define_cong_common_non_trivial_classes(jl::Module& m) { - using Word = typename Thing::native_word_type; - - m.method("cong_common_non_trivial_classes", - [](Thing& x, Thing& y) -> std::vector> { - return libsemigroups::congruence_common::non_trivial_classes( - x, y); - }); - } - } // namespace - void define_cong_common(jl::Module& m) { using libsemigroups::Runner; using CongruenceCommon = libsemigroups::detail::CongruenceCommon; // No constructors: CongruenceCommon is a shared implementation base. - // Derived algorithms register their concrete methods on their own types. + // Derived algorithms register their concrete methods on their own types, + // and instantiate the cong-common helper templates from cong-common.hpp + // in their own translation units (see knuth-bendix.cpp, + // todd-coxeter.cpp, ...). m.add_type("CongruenceCommon", jlcxx::julia_base_type()); } - void define_knuth_bendix_cong_common_helpers(jl::Module& m) { - using libsemigroups::word_type; - using KB = libsemigroups::KnuthBendix; - - define_cong_common_word_helpers(m); - define_cong_common_normal_forms(m); - define_cong_common_non_trivial_classes(m); - } - } // namespace libsemigroups_julia diff --git a/deps/src/cong-common.hpp b/deps/src/cong-common.hpp new file mode 100644 index 0000000..d298f6b --- /dev/null +++ b/deps/src/cong-common.hpp @@ -0,0 +1,162 @@ +// +// Semigroups.jl +// Copyright (C) 2026, James W. Swent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +// Shared template family used to instantiate the cong-common helper +// dispatch shims for each concrete congruence-algorithm type (KnuthBendix, +// ToddCoxeter, ...). The actual user-facing Julia helpers in +// `src/cong-common.jl` dispatch on the `CongruenceCommon` supertype, so +// once a derived algorithm registers these shims for its type, all common +// helpers (`reduce`, `contains`, `normal_forms`, `partition`, +// `non_trivial_classes`, ...) work on it for free. +// +// INCLUDE-ORDER REQUIREMENT (silent runtime bug if violated): +// Translation units that instantiate these templates MUST include the +// algorithm-specific helpers header (e.g. +// `` or +// ``) BEFORE including this file. +// The calls to `congruence_common::reduce`, `non_trivial_classes`, etc. are +// resolved by ADL at template-instantiation time; if the algorithm-specific +// overload isn't visible in the TU, the call silently binds to the generic +// base in ``. The link succeeds and +// the wrong code runs at runtime. See `cong-common.cpp` (KnuthBendix) and +// `todd-coxeter.cpp` (ToddCoxeter) for the established pattern; future +// bindings (Kambites, Congruence, ...) must follow it. + +#ifndef LIBSEMIGROUPS_JULIA_CONG_COMMON_HPP_ +#define LIBSEMIGROUPS_JULIA_CONG_COMMON_HPP_ + +// CRITICAL: libsemigroups_julia.hpp MUST be included first (fmt consteval fix) +#include "libsemigroups_julia.hpp" + +#include + +#include + +#include +#include + +namespace libsemigroups_julia { + + template + inline void define_cong_common_word_helpers(jl::Module& m) { + using Word = typename Thing::native_word_type; + + // reduce (triggers full enumeration) + m.method("cong_common_reduce", + [](Thing& self, jlcxx::ArrayRef w) -> Word { + Word input(w.begin(), w.end()); + return libsemigroups::congruence_common::reduce(self, input); + }); + + // reduce_no_run (no enumeration) + m.method("cong_common_reduce_no_run", + [](Thing const& self, jlcxx::ArrayRef w) -> Word { + Word input(w.begin(), w.end()); + return libsemigroups::congruence_common::reduce_no_run(self, + input); + }); + + // contains (triggers full enumeration) + m.method("cong_common_contains", + [](Thing& self, + jlcxx::ArrayRef u, + jlcxx::ArrayRef v) -> bool { + Word uw(u.begin(), u.end()); + Word vw(v.begin(), v.end()); + return libsemigroups::congruence_common::contains(self, uw, vw); + }); + + // currently_contains (no enumeration, returns tril) + m.method("cong_common_currently_contains", + [](Thing const& self, + jlcxx::ArrayRef u, + jlcxx::ArrayRef v) -> libsemigroups::tril { + Word uw(u.begin(), u.end()); + Word vw(v.begin(), v.end()); + return libsemigroups::congruence_common::currently_contains( + self, uw, vw); + }); + + // add_generating_pair! + m.method( + "cong_common_add_generating_pair!", + [](Thing& self, jlcxx::ArrayRef u, jlcxx::ArrayRef v) { + Word uw(u.begin(), u.end()); + Word vw(v.begin(), v.end()); + libsemigroups::congruence_common::add_generating_pair(self, uw, vw); + }); + + m.method("cong_common_partition", + [](Thing& self, jlcxx::ArrayRef words) + -> std::vector> { + std::vector input; + input.reserve(words.size()); + for (jl_value_t* word_value : words) { + auto word = jlcxx::ArrayRef( + reinterpret_cast(word_value)); + input.emplace_back(word.begin(), word.end()); + } + return libsemigroups::congruence_common::partition( + self, input.begin(), input.end()); + }); + } + + template + inline void define_cong_common_normal_forms(jl::Module& m) { + using Word = typename Thing::native_word_type; + + // normal_forms() returns an rx-style range; use + // .at_end()/.get()/.next(). + m.method("cong_common_normal_forms", [](Thing& self) -> std::vector { + std::vector result; + auto range = libsemigroups::congruence_common::normal_forms(self); + while (!range.at_end()) { + result.push_back(range.get()); + range.next(); + } + return result; + }); + } + + template + inline void define_cong_common_non_trivial_classes(jl::Module& m) { + using Word = typename Thing::native_word_type; + + m.method("cong_common_non_trivial_classes", + [](Thing& x, Thing& y) -> std::vector> { + return libsemigroups::congruence_common::non_trivial_classes(x, + y); + }); + } + + // Aggregator: register the full cong-common helper family for one + // concrete algorithm type. Each new algorithm binding (KnuthBendix, + // ToddCoxeter, Kambites, ...) calls this once at the end of its + // `define_(m)` function. The include-order requirement + // documented above still applies - the algorithm-specific helpers + // header must be included before this file in the calling TU. + template + inline void define_cong_common_helpers(jl::Module& m) { + define_cong_common_word_helpers(m); + define_cong_common_normal_forms(m); + define_cong_common_non_trivial_classes(m); + } + +} // namespace libsemigroups_julia + +#endif // LIBSEMIGROUPS_JULIA_CONG_COMMON_HPP_ diff --git a/deps/src/knuth-bendix.cpp b/deps/src/knuth-bendix.cpp index fbe6da3..c3b80c2 100644 --- a/deps/src/knuth-bendix.cpp +++ b/deps/src/knuth-bendix.cpp @@ -19,11 +19,16 @@ // CRITICAL: libsemigroups_julia.hpp MUST be included first (fmt consteval fix) #include "libsemigroups_julia.hpp" +// knuth-bendix-helpers.hpp MUST come BEFORE cong-common.hpp so the template +// bodies in cong-common.hpp see KB-specific overloads of congruence_common +// helpers (e.g., non_trivial_classes(KB&, KB&)) at instantiation time. #include #include #include #include +#include "cong-common.hpp" + #include #include #include @@ -256,6 +261,8 @@ namespace libsemigroups_julia { p, std::chrono::nanoseconds(ns)); return static_cast(std::distance(p.rules.cbegin(), it)); }); + + define_cong_common_helpers(m); } } // namespace libsemigroups_julia diff --git a/deps/src/libsemigroups_julia.cpp b/deps/src/libsemigroups_julia.cpp index 7eab2d7..94b2d52 100644 --- a/deps/src/libsemigroups_julia.cpp +++ b/deps/src/libsemigroups_julia.cpp @@ -54,7 +54,7 @@ namespace libsemigroups_julia { define_presentation(mod); define_presentation_examples(mod); define_knuth_bendix(mod); - define_knuth_bendix_cong_common_helpers(mod); + define_todd_coxeter(mod); } } // namespace libsemigroups_julia diff --git a/deps/src/libsemigroups_julia.hpp b/deps/src/libsemigroups_julia.hpp index 026e99c..85b92e1 100644 --- a/deps/src/libsemigroups_julia.hpp +++ b/deps/src/libsemigroups_julia.hpp @@ -22,7 +22,7 @@ #ifndef LIBSEMIGROUPS_JULIA_HPP_ #define LIBSEMIGROUPS_JULIA_HPP_ -// JlCxx headers FIRST — these pull in standard library headers (, +// JlCxx headers FIRST - these pull in standard library headers (, // , etc.) that on libstdc++ use __cpp_lib_is_constant_evaluated to // decide constexpr-ness. They must be included while the macro is intact. #include "jlcxx/jlcxx.hpp" @@ -69,7 +69,7 @@ namespace libsemigroups_julia { void define_presentation(jl::Module& mod); void define_presentation_examples(jl::Module& mod); void define_knuth_bendix(jl::Module& mod); - void define_knuth_bendix_cong_common_helpers(jl::Module& mod); + void define_todd_coxeter(jl::Module& mod); } // namespace libsemigroups_julia diff --git a/deps/src/todd-coxeter.cpp b/deps/src/todd-coxeter.cpp new file mode 100644 index 0000000..bd259da --- /dev/null +++ b/deps/src/todd-coxeter.cpp @@ -0,0 +1,317 @@ +// +// Semigroups.jl +// Copyright (C) 2026, James W. Swent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +// CRITICAL: libsemigroups_julia.hpp MUST be included first (fmt consteval fix) +#include "libsemigroups_julia.hpp" + +// todd-coxeter-helpers.hpp MUST come BEFORE cong-common.hpp so the template +// bodies in cong-common.hpp see TC-specific overloads of +// congruence_common helpers (e.g., non_trivial_classes(TC&, TC&)). +#include +#include +#include +#include +#include + +#include "cong-common.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace jlcxx { + template <> + struct IsMirroredType + : std::false_type {}; + + template <> + struct IsMirroredType> + : std::false_type {}; + + template <> + struct SuperType { + using type = libsemigroups::detail::CongruenceCommon; + }; + + template <> + struct SuperType> { + using type = libsemigroups::detail::ToddCoxeterImpl; + }; +} // namespace jlcxx + +namespace libsemigroups_julia { + + void define_todd_coxeter(jl::Module& m) { + using libsemigroups::congruence_kind; + using libsemigroups::Order; + using libsemigroups::Presentation; + using libsemigroups::word_type; + using libsemigroups::WordGraph; + + using CongruenceCommon = libsemigroups::detail::CongruenceCommon; + using TCImpl = libsemigroups::detail::ToddCoxeterImpl; + using TC = libsemigroups::ToddCoxeter; + + // strategy: TCImpl::options::strategy has 8 values; we only expose the + // 6 that appear on the user-facing ToddCoxeter::options::strategy. + m.add_bits("strategy", + jl::julia_type("CppEnum")); + m.set_const("strategy_hlt", TCImpl::options::strategy::hlt); + m.set_const("strategy_felsch", TCImpl::options::strategy::felsch); + m.set_const("strategy_CR", TCImpl::options::strategy::CR); + m.set_const("strategy_R_over_C", TCImpl::options::strategy::R_over_C); + m.set_const("strategy_Cr", TCImpl::options::strategy::Cr); + m.set_const("strategy_Rc", TCImpl::options::strategy::Rc); + + // lookahead_extent + m.add_bits("lookahead_extent", + jl::julia_type("CppEnum")); + m.set_const("lookahead_extent_full", + TCImpl::options::lookahead_extent::full); + m.set_const("lookahead_extent_partial", + TCImpl::options::lookahead_extent::partial); + + // lookahead_style + m.add_bits("lookahead_style", + jl::julia_type("CppEnum")); + m.set_const("lookahead_style_hlt", TCImpl::options::lookahead_style::hlt); + m.set_const("lookahead_style_felsch", + TCImpl::options::lookahead_style::felsch); + + // def_policy + m.add_bits("def_policy", + jl::julia_type("CppEnum")); + m.set_const("def_policy_no_stack_if_no_space", + TCImpl::options::def_policy::no_stack_if_no_space); + m.set_const("def_policy_purge_from_top", + TCImpl::options::def_policy::purge_from_top); + m.set_const("def_policy_purge_all", TCImpl::options::def_policy::purge_all); + m.set_const("def_policy_discard_all_if_no_space", + TCImpl::options::def_policy::discard_all_if_no_space); + m.set_const("def_policy_unlimited", TCImpl::options::def_policy::unlimited); + + // def_version (re-exported into TCImpl::options via a using-declaration + // from FelschGraphSettings::options) + m.add_bits("def_version", + jl::julia_type("CppEnum")); + m.set_const("def_version_one", TCImpl::options::def_version::one); + m.set_const("def_version_two", TCImpl::options::def_version::two); + + // Type registration + m.add_type("ToddCoxeterImpl", + jlcxx::julia_base_type()); + auto type + = m.add_type("ToddCoxeterWord", jlcxx::julia_base_type()); + + // Constructors + type.constructor const&>(); + type.constructor(); + type.constructor const&>(); + type.constructor(); // copy ctor + + // init! overloads (mirror constructors) + type.method("init!", [](TC& self) -> TC& { return self.init(); }); + type.method("init!", + [](TC& self, + congruence_kind knd, + Presentation const& p) -> TC& { + return self.init(knd, p); + }); + type.method("init!", + [](TC& self, congruence_kind knd, TC const& other) -> TC& { + return self.init(knd, other); + }); + type.method("init!", + [](TC& self, congruence_kind knd, WordGraph const& wg) + -> TC& { return self.init(knd, wg); }); + + // strategy + type.method("strategy", [](TC const& self) -> TCImpl::options::strategy { + return self.strategy(); + }); + type.method("set_strategy!", [](TC& self, TCImpl::options::strategy val) { + self.strategy(val); + }); + + // lookahead_extent + type.method("lookahead_extent", + [](TC const& self) -> TCImpl::options::lookahead_extent { + return self.lookahead_extent(); + }); + type.method("set_lookahead_extent!", + [](TC& self, TCImpl::options::lookahead_extent val) { + self.lookahead_extent(val); + }); + + // lookahead_style + type.method("lookahead_style", + [](TC const& self) -> TCImpl::options::lookahead_style { + return self.lookahead_style(); + }); + type.method("set_lookahead_style!", + [](TC& self, TCImpl::options::lookahead_style val) { + self.lookahead_style(val); + }); + + // save + type.method("save", [](TC const& self) -> bool { return self.save(); }); + type.method("set_save!", [](TC& self, bool val) { self.save(val); }); + + // use_relations_in_extra + type.method("use_relations_in_extra", [](TC const& self) -> bool { + return self.use_relations_in_extra(); + }); + type.method("set_use_relations_in_extra!", + [](TC& self, bool val) { self.use_relations_in_extra(val); }); + + // lower_bound + type.method("lower_bound", + [](TC const& self) -> size_t { return self.lower_bound(); }); + type.method("set_lower_bound!", + [](TC& self, size_t val) { self.lower_bound(val); }); + + // def_version + type.method("def_version", + [](TC const& self) -> TCImpl::options::def_version { + return self.def_version(); + }); + type.method("set_def_version!", + [](TC& self, TCImpl::options::def_version val) { + self.def_version(val); + }); + + // def_policy + type.method("def_policy", + [](TC const& self) -> TCImpl::options::def_policy { + return self.def_policy(); + }); + type.method("set_def_policy!", + [](TC& self, TCImpl::options::def_policy val) { + self.def_policy(val); + }); + + type.method("standardize!", [](TC& self, Order ord) -> bool { + return self.standardize(ord); + }); + + // is_standardized: route through current_word_graph(). The wrappers + // self.is_standardized(...) on TC/TCImpl are [[deprecated]] in + // libsemigroups, with this exact replacement recommended. + type.method("is_standardized", [](TC const& self) -> bool { + return self.current_word_graph().is_standardized(); + }); + + type.method("is_standardized", [](TC const& self, Order ord) -> bool { + return self.current_word_graph().is_standardized(ord); + }); + + // current_word_graph: large stable data, return by const reference. + type.method("current_word_graph", + [](TC const& self) -> WordGraph const& { + return self.current_word_graph(); + }); + + // word_graph: triggers run; non-const this. + type.method("word_graph", [](TC& self) -> WordGraph const& { + return self.word_graph(); + }); + + type.method("current_index_of", + [](TC const& self, jlcxx::ArrayRef w) -> size_t { + word_type ww(w.begin(), w.end()); + return self.current_index_of(ww.begin(), ww.end()); + }); + + type.method("index_of", [](TC& self, jlcxx::ArrayRef w) -> size_t { + word_type ww(w.begin(), w.end()); + return self.index_of(ww.begin(), ww.end()); + }); + + type.method("current_word_of", [](TC const& self, size_t i) -> word_type { + word_type out; + self.current_word_of(std::back_inserter(out), i); + return out; + }); + + type.method("throw_if_letter_not_in_alphabet", + [](TC const& self, jlcxx::ArrayRef w) { + word_type ww(w.begin(), w.end()); + self.throw_if_letter_not_in_alphabet(ww.begin(), ww.end()); + }); + + type.method("word_of", [](TC& self, size_t i) -> word_type { + word_type out; + self.word_of(std::back_inserter(out), i); + return out; + }); + + type.method("number_of_classes", + [](TC& self) -> uint64_t { return self.number_of_classes(); }); + + type.method("kind", + [](TC const& self) -> congruence_kind { return self.kind(); }); + + type.method("number_of_generating_pairs", [](TC const& self) -> size_t { + return self.number_of_generating_pairs(); + }); + + type.method("generating_pairs", + [](TC const& self) -> std::vector { + auto const& pairs = self.generating_pairs(); + return std::vector(pairs.begin(), pairs.end()); + }); + + // presentation - return by copy + type.method("presentation", [](TC const& self) -> Presentation { + return self.presentation(); + }); + + type.method("to_human_readable_repr", [](TC const& self) -> std::string { + return libsemigroups::to_human_readable_repr(self); + }); + + // is_non_trivial - takes nanoseconds at the boundary, converts to + // milliseconds (the helper takes std::chrono::milliseconds). + m.method("tc_is_non_trivial", + [](TC& self, size_t tries, int64_t try_for_ns, float threshold) + -> libsemigroups::tril { + auto try_for + = std::chrono::duration_cast( + std::chrono::nanoseconds(try_for_ns)); + return libsemigroups::todd_coxeter::is_non_trivial( + self, tries, try_for, threshold); + }); + + // redundant_rule - returns a 0-based index into p.rules (lhs position), + // or p.rules.size() if no redundant rule was found. + m.method("tc_redundant_rule", + [](Presentation const& p, int64_t ns) -> size_t { + auto it = libsemigroups::todd_coxeter::redundant_rule( + p, std::chrono::nanoseconds(ns)); + return static_cast(std::distance(p.rules.cbegin(), it)); + }); + + define_cong_common_helpers(m); + } + +} // namespace libsemigroups_julia diff --git a/src/Semigroups.jl b/src/Semigroups.jl index 7a70265..9d143e7 100644 --- a/src/Semigroups.jl +++ b/src/Semigroups.jl @@ -82,6 +82,7 @@ include("presentation.jl") include("presentation-examples.jl") include("cong-common.jl") include("knuth-bendix.jl") +include("todd-coxeter.jl") # High-level element types include("bmat8.jl") @@ -353,6 +354,27 @@ export active_rules, gilman_graph, gilman_graph_node_labels export by_overlap_length!, is_reduced, redundant_rule export normal_forms, partition, non_trivial_classes +# ToddCoxeter +export ToddCoxeter +export strategy_hlt, + strategy_felsch, strategy_CR, strategy_R_over_C, strategy_Cr, strategy_Rc +export lookahead_extent_full, lookahead_extent_partial +export lookahead_style_hlt, lookahead_style_felsch +export def_policy_no_stack_if_no_space, def_policy_purge_from_top, def_policy_purge_all +export def_policy_discard_all_if_no_space, def_policy_unlimited +export def_version_one, def_version_two +export strategy, strategy! +export lookahead_extent, lookahead_extent! +export lookahead_style, lookahead_style! +export save, save! +export use_relations_in_extra, use_relations_in_extra! +export lower_bound, lower_bound! +export def_version, def_version! +export def_policy, def_policy! +export standardize!, is_standardized, current_word_graph, word_graph +export current_index_of, word_of, current_word_of +export is_non_trivial, tc_redundant_rule + # Transformation types and functions export Transf, PPerm, Perm export degree, rank, image, domain, inverse diff --git a/src/todd-coxeter.jl b/src/todd-coxeter.jl new file mode 100644 index 0000000..c0fc9c7 --- /dev/null +++ b/src/todd-coxeter.jl @@ -0,0 +1,1013 @@ +# Copyright (c) 2026, James W. Swent +# +# Distributed under the terms of the GPL license version 3. + +""" +todd-coxeter.jl - ToddCoxeter wrapper (Layer 2 + 3) +""" + +# ============================================================================ +# Type alias and enum constants +# ============================================================================ + +""" + ToddCoxeter + +Type implementing the Todd-Coxeter algorithm for computing 1- and 2-sided +congruences on finitely presented semigroups and monoids. + +A `ToddCoxeter` object represents a coset enumeration over a presentation, +producing a [`WordGraph`](@ref Semigroups.WordGraph) whose nodes correspond +to the congruence classes. It is constructed from a +[`congruence_kind`](@ref Semigroups.congruence_kind) and a +[`Presentation`](@ref Semigroups.Presentation), or from another +`ToddCoxeter` (a quotient construction), or from a `WordGraph`. + +`ToddCoxeter` is a subtype of [`CongruenceCommon`](@ref +Semigroups.CongruenceCommon) (and hence of [`Runner`](@ref Semigroups.Runner)), +so all runner methods (`run!`, `run_for!`, `run_until!`, `finished`, +`timed_out`, `current_state`, etc.) and all common congruence helpers +(`reduce`, `contains`, `currently_contains`, `add_generating_pair!`, +`normal_forms`, `partition`, `non_trivial_classes`) work on `ToddCoxeter` +objects. + +# Constructors + + ToddCoxeter(kind::congruence_kind, p::Presentation) -> ToddCoxeter + ToddCoxeter(kind::congruence_kind, tc::ToddCoxeter) -> ToddCoxeter + ToddCoxeter(kind::congruence_kind, wg::WordGraph) -> ToddCoxeter + ToddCoxeter(other::ToddCoxeter) -> ToddCoxeter + +# Throws + +- `LibsemigroupsError` if `p` is not valid (Presentation form). +- `LibsemigroupsError` if `kind` and `tc.kind()` are incompatible + (ToddCoxeter form). Compatible pairs `(tc.kind(), kind)` are + `(onesided, onesided)`, `(twosided, onesided)`, and `(twosided, twosided)`. + +!!! warning "v1 limitation" + v1 of Semigroups.jl binds `ToddCoxeter{word_type}` only. String-alphabet + presentations are deferred to v1.1. +""" +const ToddCoxeter = LibSemigroups.ToddCoxeterWord + +# --- strategy --------------------------------------------------------------- + +""" + strategy_hlt + +Strategy enum value: the HLT (Hazelgrove-Leech-Trotter) strategy. Analogous +to the [ACE](https://staff.itee.uq.edu.au/havas/) R-style strategy. See +[`strategy!`](@ref Semigroups.strategy!). +""" +const strategy_hlt = LibSemigroups.strategy_hlt + +""" + strategy_felsch + +Strategy enum value: the Felsch strategy. Analogous to the +[ACE](https://staff.itee.uq.edu.au/havas/) C-style strategy. See +[`strategy!`](@ref Semigroups.strategy!). +""" +const strategy_felsch = LibSemigroups.strategy_felsch + +""" + strategy_CR + +Strategy enum value: mimics the [ACE](https://staff.itee.uq.edu.au/havas/) CR +strategy. The Felsch strategy is run until at least `f_defs()` nodes are +defined, then HLT is run until at least `hlt_defs()/N` nodes have been defined +(where `N` is the sum of the lengths of the words in the presentation and +generating pairs); these steps are repeated until enumeration terminates. See +[`strategy!`](@ref Semigroups.strategy!). +""" +const strategy_CR = LibSemigroups.strategy_CR + +""" + strategy_R_over_C + +Strategy enum value: mimics the [ACE](https://staff.itee.uq.edu.au/havas/) R/C +strategy. HLT runs until the first lookahead is triggered (when the number of +active nodes reaches `lookahead_next`); a full lookahead is then performed, +after which the [`strategy_CR`](@ref Semigroups.strategy_CR) strategy is used. +See [`strategy!`](@ref Semigroups.strategy!). +""" +const strategy_R_over_C = LibSemigroups.strategy_R_over_C + +""" + strategy_Cr + +Strategy enum value: mimics the [ACE](https://staff.itee.uq.edu.au/havas/) Cr +strategy. Felsch runs until at least `f_defs()` new nodes have been defined, +HLT runs until at least `hlt_defs()/N` further nodes have been defined (`N` +as in [`strategy_CR`](@ref Semigroups.strategy_CR)), and finally Felsch runs +to completion. See [`strategy!`](@ref Semigroups.strategy!). +""" +const strategy_Cr = LibSemigroups.strategy_Cr + +""" + strategy_Rc + +Strategy enum value: mimics the [ACE](https://staff.itee.uq.edu.au/havas/) Rc +strategy. HLT runs until at least `hlt_defs()/N` new nodes have been defined +(`N` as in [`strategy_CR`](@ref Semigroups.strategy_CR)), Felsch runs until +at least `f_defs()` further nodes have been defined, and finally HLT runs to +completion. See [`strategy!`](@ref Semigroups.strategy!). +""" +const strategy_Rc = LibSemigroups.strategy_Rc + +# --- lookahead_extent ------------------------------------------------------- + +""" + lookahead_extent_full + +Lookahead-extent enum value: perform a full lookahead from every node in the +word graph. Full lookaheads are sometimes slower but may detect more +coincidences than a partial lookahead. See +[`lookahead_extent!`](@ref Semigroups.lookahead_extent!). +""" +const lookahead_extent_full = LibSemigroups.lookahead_extent_full + +""" + lookahead_extent_partial + +Lookahead-extent enum value: perform a partial lookahead starting from the +current node in the word graph. Partial lookaheads are sometimes faster but +may not detect as many coincidences as a full lookahead. See +[`lookahead_extent!`](@ref Semigroups.lookahead_extent!). +""" +const lookahead_extent_partial = LibSemigroups.lookahead_extent_partial + +# --- lookahead_style -------------------------------------------------------- + +""" + lookahead_style_hlt + +Lookahead-style enum value: HLT-style lookahead - follow the paths labelled by +every relation from every node in the range specified by the current +[`lookahead_extent`](@ref Semigroups.lookahead_extent). See +[`lookahead_style!`](@ref Semigroups.lookahead_style!). +""" +const lookahead_style_hlt = LibSemigroups.lookahead_style_hlt + +""" + lookahead_style_felsch + +Lookahead-style enum value: Felsch-style lookahead - every edge is considered +in every path labelled by a relation in which it occurs. See +[`lookahead_style!`](@ref Semigroups.lookahead_style!). +""" +const lookahead_style_felsch = LibSemigroups.lookahead_style_felsch + +# --- def_policy ------------------------------------------------------------- + +""" + def_policy_no_stack_if_no_space + +Definition-policy enum value: when the definition stack reaches the limit +`def_max`, newly generated definitions are dropped on the floor. See +[`def_policy!`](@ref Semigroups.def_policy!). +""" +const def_policy_no_stack_if_no_space = LibSemigroups.def_policy_no_stack_if_no_space + +""" + def_policy_purge_from_top + +Definition-policy enum value: when the stack is full and a new definition is +generated, definitions whose source node is dead are popped from the top of +the stack. See [`def_policy!`](@ref Semigroups.def_policy!). +""" +const def_policy_purge_from_top = LibSemigroups.def_policy_purge_from_top + +""" + def_policy_purge_all + +Definition-policy enum value: when the stack is full and a new definition is +generated, all definitions with a dead source node are removed from the +entire stack (not just the top). See [`def_policy!`](@ref Semigroups.def_policy!). +""" +const def_policy_purge_all = LibSemigroups.def_policy_purge_all + +""" + def_policy_discard_all_if_no_space + +Definition-policy enum value: when the stack is full and a new definition is +generated, the entire definition stack is discarded. See +[`def_policy!`](@ref Semigroups.def_policy!). +""" +const def_policy_discard_all_if_no_space = LibSemigroups.def_policy_discard_all_if_no_space + +""" + def_policy_unlimited + +Definition-policy enum value: place no limit on the number of stacked +definitions. See [`def_policy!`](@ref Semigroups.def_policy!). +""" +const def_policy_unlimited = LibSemigroups.def_policy_unlimited + +# --- def_version ------------------------------------------------------------ + +""" + def_version_one + +Definition-version enum value: the simpler version of definition processing. +May follow the same dead-end path multiple times. See +[`def_version!`](@ref Semigroups.def_version!). +""" +const def_version_one = LibSemigroups.def_version_one + +""" + def_version_two + +Definition-version enum value: the more complex version of definition +processing. Attempts to avoid re-following a path once it has been found to +lead nowhere. See [`def_version!`](@ref Semigroups.def_version!). +""" +const def_version_two = LibSemigroups.def_version_two + +# ============================================================================ +# Class-index conversion (private, file-local) +# ============================================================================ +# C++ uses 0-based class indices and `typemax(size_t)` for UNDEFINED. These +# helpers translate at the Julia<->C++ boundary; keep their calls *outside* +# `@wrap_libsemigroups_call` so that native Julia errors (e.g., `InexactError` +# from `UInt(0 - 1)`) propagate as themselves rather than being re-wrapped. + +@inline _index_to_cpp(i::Integer) = UInt(i - 1) +@inline _index_from_cpp(i::Integer) = i == typemax(UInt) ? UNDEFINED : Int(i) + 1 + +# ============================================================================ +# Initialization +# ============================================================================ + +""" + init!(tc::ToddCoxeter) -> ToddCoxeter + init!(tc::ToddCoxeter, kind::congruence_kind, p::Presentation) -> ToddCoxeter + init!(tc::ToddCoxeter, kind::congruence_kind, other::ToddCoxeter) -> ToddCoxeter + init!(tc::ToddCoxeter, kind::congruence_kind, wg::WordGraph) -> ToddCoxeter + +Re-initialize `tc` so that it is in the state it would have been in +immediately after the corresponding constructor. + +The one-argument form clears the underlying word graph, presentation, +generating pairs, settings, and statistics, putting `tc` back into the same +state as a newly default-constructed +[`ToddCoxeter`](@ref Semigroups.ToddCoxeter). + +The three-argument forms reinitialize `tc` from the corresponding arguments +- `(kind, p)` for a [`Presentation`](@ref Semigroups.Presentation), +`(kind, other)` for a quotient of another `ToddCoxeter`, or `(kind, wg)` for +a [`WordGraph`](@ref Semigroups.WordGraph). + +Returns `tc` for chaining. + +# Throws + +- `LibsemigroupsError` if `p` is not valid (Presentation form). +- `LibsemigroupsError` if `kind` and `other.kind()` are incompatible + (ToddCoxeter form). The compatible pairs `(other.kind(), kind)` are + `(onesided, onesided)`, `(twosided, onesided)`, and `(twosided, twosided)`. +""" +function init!(tc::ToddCoxeter) + @wrap_libsemigroups_call LibSemigroups.init!(tc) + return tc +end + +function init!(tc::ToddCoxeter, kind::congruence_kind, p::Presentation) + @wrap_libsemigroups_call LibSemigroups.init!(tc, kind, p) + return tc +end + +function init!(tc::ToddCoxeter, kind::congruence_kind, other::ToddCoxeter) + @wrap_libsemigroups_call LibSemigroups.init!(tc, kind, other) + return tc +end + +function init!(tc::ToddCoxeter, kind::congruence_kind, wg::WordGraph) + @wrap_libsemigroups_call LibSemigroups.init!(tc, kind, wg) + return tc +end + +# ============================================================================ +# Settings - getter / setter pairs +# ============================================================================ + +""" + strategy(tc::ToddCoxeter) + +Return the current coset enumeration strategy of `tc`. + +The returned value is one of [`strategy_hlt`](@ref Semigroups.strategy_hlt) +(the default), [`strategy_felsch`](@ref Semigroups.strategy_felsch), +[`strategy_CR`](@ref Semigroups.strategy_CR), +[`strategy_R_over_C`](@ref Semigroups.strategy_R_over_C), +[`strategy_Cr`](@ref Semigroups.strategy_Cr), or +[`strategy_Rc`](@ref Semigroups.strategy_Rc). + +# See also + +[`strategy!`](@ref Semigroups.strategy!) +""" +strategy(tc::ToddCoxeter) = LibSemigroups.strategy(tc) + +""" + strategy!(tc::ToddCoxeter, val) -> ToddCoxeter + +Set the coset enumeration strategy of `tc` to `val`. Returns `tc` for +chaining. `val` must be one of the `strategy_*` enum constants. + +# See also + +[`strategy`](@ref Semigroups.strategy) +""" +function strategy!(tc::ToddCoxeter, val) + LibSemigroups.set_strategy!(tc, val) + return tc +end + +""" + lookahead_extent(tc::ToddCoxeter) + +Return the current lookahead extent of `tc`. The default is +[`lookahead_extent_partial`](@ref Semigroups.lookahead_extent_partial). + +The returned value is one of +[`lookahead_extent_full`](@ref Semigroups.lookahead_extent_full) or +[`lookahead_extent_partial`](@ref Semigroups.lookahead_extent_partial). + +# See also + +[`lookahead_extent!`](@ref Semigroups.lookahead_extent!) +""" +lookahead_extent(tc::ToddCoxeter) = LibSemigroups.lookahead_extent(tc) + +""" + lookahead_extent!(tc::ToddCoxeter, val) -> ToddCoxeter + +Set the lookahead extent of `tc` to `val`. Returns `tc` for chaining. + +# See also + +[`lookahead_extent`](@ref Semigroups.lookahead_extent) +""" +function lookahead_extent!(tc::ToddCoxeter, val) + LibSemigroups.set_lookahead_extent!(tc, val) + return tc +end + +""" + lookahead_style(tc::ToddCoxeter) + +Return the current lookahead style of `tc`. The default is +[`lookahead_style_hlt`](@ref Semigroups.lookahead_style_hlt). + +The returned value is one of +[`lookahead_style_hlt`](@ref Semigroups.lookahead_style_hlt) or +[`lookahead_style_felsch`](@ref Semigroups.lookahead_style_felsch). + +# See also + +[`lookahead_style!`](@ref Semigroups.lookahead_style!) +""" +lookahead_style(tc::ToddCoxeter) = LibSemigroups.lookahead_style(tc) + +""" + lookahead_style!(tc::ToddCoxeter, val) -> ToddCoxeter + +Set the lookahead style of `tc` to `val`. Returns `tc` for chaining. + +# See also + +[`lookahead_style`](@ref Semigroups.lookahead_style) +""" +function lookahead_style!(tc::ToddCoxeter, val) + LibSemigroups.set_lookahead_style!(tc, val) + return tc +end + +""" + save(tc::ToddCoxeter) -> Bool + +Return whether definitions are processed during HLT-style enumeration. The +default is `false`. + +# See also + +[`save!`](@ref Semigroups.save!) +""" +save(tc::ToddCoxeter) = LibSemigroups.save(tc) + +""" + save!(tc::ToddCoxeter, val::Bool) -> ToddCoxeter + +Set whether definitions are processed during any HLT-style enumeration of +`tc`. Returns `tc` for chaining. The default is `false`. + +If `val` is `true` and the HLT strategy is in use, definitions are processed +during enumeration. This typically slows HLT down but can reduce the size of +the underlying word graph. + +# See also + +[`save`](@ref Semigroups.save) +""" +function save!(tc::ToddCoxeter, val::Bool) + LibSemigroups.set_save!(tc, val) + return tc +end + +""" + use_relations_in_extra(tc::ToddCoxeter) -> Bool + +Return whether, when the Felsch strategy is in use over a finitely presented +semigroup or monoid, the algorithm performs an HLT-style push of the defining +relations at the identity. The default is `false`. + +# See also + +[`use_relations_in_extra!`](@ref Semigroups.use_relations_in_extra!) +""" +use_relations_in_extra(tc::ToddCoxeter) = LibSemigroups.use_relations_in_extra(tc) + +""" + use_relations_in_extra!(tc::ToddCoxeter, val::Bool) -> ToddCoxeter + +Set whether, when the Felsch strategy is in use over a finitely presented +semigroup or monoid, the algorithm should follow all paths from the identity +labelled by the underlying relations. Returns `tc` for chaining. The default +is `false`. + +# See also + +[`use_relations_in_extra`](@ref Semigroups.use_relations_in_extra) +""" +function use_relations_in_extra!(tc::ToddCoxeter, val::Bool) + LibSemigroups.set_use_relations_in_extra!(tc, val) + return tc +end + +""" + lower_bound(tc::ToddCoxeter) -> Union{Int, UndefinedType} + +Return the current lower bound on the number of classes of the congruence +represented by `tc`. The default is [`UNDEFINED`](@ref Semigroups.UNDEFINED) +(no bound set); otherwise an `Int`. + +# See also + +[`lower_bound!`](@ref Semigroups.lower_bound!) +""" +function lower_bound(tc::ToddCoxeter) + val = LibSemigroups.lower_bound(tc) + return val == typemax(UInt) ? UNDEFINED : Int(val) +end + +""" + lower_bound!(tc::ToddCoxeter, val::Integer) -> ToddCoxeter + lower_bound!(tc::ToddCoxeter, ::UndefinedType) -> ToddCoxeter + +Set a lower bound on the number of classes of the congruence represented by +`tc`. Returns `tc` for chaining. + +If the number of active nodes during enumeration reaches `val` and the word +graph is complete, the algorithm may stop early. When the bound equals the +number of classes, this can avoid following relation-labelled paths at many +nodes once no further coincidences are possible. Pass +[`UNDEFINED`](@ref Semigroups.UNDEFINED) to clear the bound (the default). + +# See also + +[`lower_bound`](@ref Semigroups.lower_bound) +""" +function lower_bound!(tc::ToddCoxeter, val::Integer) + LibSemigroups.set_lower_bound!(tc, UInt(val)) + return tc +end + +lower_bound!(tc::ToddCoxeter, ::UndefinedType) = + (LibSemigroups.set_lower_bound!(tc, typemax(UInt)); tc) + +""" + def_version(tc::ToddCoxeter) + +Return the current definition-routine version of `tc`. The default is +[`def_version_two`](@ref Semigroups.def_version_two). + +The returned value is one of +[`def_version_one`](@ref Semigroups.def_version_one) or +[`def_version_two`](@ref Semigroups.def_version_two). + +# See also + +[`def_version!`](@ref Semigroups.def_version!) +""" +def_version(tc::ToddCoxeter) = LibSemigroups.def_version(tc) + +""" + def_version!(tc::ToddCoxeter, val) -> ToddCoxeter + +Set the definition-routine version of `tc` to `val`. Returns `tc` for +chaining. + +# See also + +[`def_version`](@ref Semigroups.def_version) +""" +function def_version!(tc::ToddCoxeter, val) + LibSemigroups.set_def_version!(tc, val) + return tc +end + +""" + def_policy(tc::ToddCoxeter) + +Return the current definition-stack policy of `tc`. The default is +[`def_policy_no_stack_if_no_space`](@ref Semigroups.def_policy_no_stack_if_no_space). +Together with `def_max` (currently not bound) the policy controls what +happens when the stack of pending definitions becomes full. + +The returned value is one of +[`def_policy_no_stack_if_no_space`](@ref Semigroups.def_policy_no_stack_if_no_space), +[`def_policy_purge_from_top`](@ref Semigroups.def_policy_purge_from_top), +[`def_policy_purge_all`](@ref Semigroups.def_policy_purge_all), +[`def_policy_discard_all_if_no_space`](@ref Semigroups.def_policy_discard_all_if_no_space), +or [`def_policy_unlimited`](@ref Semigroups.def_policy_unlimited). + +# See also + +[`def_policy!`](@ref Semigroups.def_policy!) +""" +def_policy(tc::ToddCoxeter) = LibSemigroups.def_policy(tc) + +""" + def_policy!(tc::ToddCoxeter, val) -> ToddCoxeter + +Set the definition-stack policy of `tc` to `val`. Returns `tc` for +chaining. + +# See also + +[`def_policy`](@ref Semigroups.def_policy) +""" +function def_policy!(tc::ToddCoxeter, val) + LibSemigroups.set_def_policy!(tc, val) + return tc +end + +# ============================================================================ +# Standardize / word-graph access +# ============================================================================ + +""" + standardize!(tc::ToddCoxeter, ord::Order) -> Bool + +Standardize the [`current_word_graph`](@ref Semigroups.current_word_graph) of +`tc` with respect to the order `ord`. This function does **not** trigger any +enumeration. + +Returns `true` if the word graph was modified - equivalently, if it was not +already standardized with respect to `ord` - and `false` otherwise. + +# Arguments + +- `tc::ToddCoxeter`: the `ToddCoxeter` instance to standardize. +- `ord::Order`: the order to standardize by - typically + [`ORDER_SHORTLEX`](@ref Semigroups.ORDER_SHORTLEX) or + [`ORDER_LEX`](@ref Semigroups.ORDER_LEX). + +# See also + +[`is_standardized`](@ref Semigroups.is_standardized) +""" +function standardize!(tc::ToddCoxeter, ord::Order) + return @wrap_libsemigroups_call LibSemigroups.standardize!(tc, ord) +end + +""" + is_standardized(tc::ToddCoxeter) -> Bool + is_standardized(tc::ToddCoxeter, ord::Order) -> Bool + +Return whether the [`current_word_graph`](@ref Semigroups.current_word_graph) +of `tc` is standardized. + +The one-argument form returns `true` if the word graph has been standardized +with respect to any [`Order`](@ref Semigroups.Order) other than +[`ORDER_NONE`](@ref Semigroups.ORDER_NONE). The two-argument form returns +`true` only if it has been standardized with respect to `ord`. + +!!! warning "Deprecated upstream" + The corresponding libsemigroups members are marked deprecated; they + suggest using `current_word_graph(tc).is_standardized(...)` directly. The + Julia binding still exposes both forms so existing code continues to + compile, but new code should prefer the word-graph form. + +# See also + +[`standardize!`](@ref Semigroups.standardize!) +""" +is_standardized(tc::ToddCoxeter) = LibSemigroups.is_standardized(tc) + +is_standardized(tc::ToddCoxeter, ord::Order) = LibSemigroups.is_standardized(tc, ord) + +""" + current_word_graph(tc::ToddCoxeter) -> WordGraph + +Return the underlying word graph of `tc` in its current state, without +triggering an enumeration. + +The returned graph may be in a complicated state: nothing is guaranteed about +the labels of active nodes (they may be any non-negative integers in any +order), about whether the active node count matches the total node count, +about completeness, or about compatibility with the relations of +[`presentation`](@ref Semigroups.presentation) or with +[`generating_pairs`](@ref Semigroups.generating_pairs). Use +[`standardize!`](@ref Semigroups.standardize!) (or `shrink_to_fit`, not yet +bound) to put it into a more reasonable state. + +# See also + +[`word_graph`](@ref Semigroups.word_graph) +""" +@cxxdereference current_word_graph(tc::ToddCoxeter) = LibSemigroups.current_word_graph(tc) + +""" + word_graph(tc::ToddCoxeter) -> WordGraph + +Return the underlying word graph of `tc` after triggering a full +enumeration. The returned graph is short-lex standardized: its active nodes +are exactly `0, ..., n-1` where `n` is +[`number_of_classes`](@ref Semigroups.number_of_classes) (or `n+1` when the +underlying [`presentation`](@ref Semigroups.presentation) does not contain +the empty word). It will usually be complete and compatible with the +relations of `presentation` and with `generating_pairs`, though some +combinations of settings (e.g. an aggressive +[`lower_bound!`](@ref Semigroups.lower_bound!)) can break this. + +!!! warning + This function may never terminate if the congruence is undecidable. + +# See also + +[`current_word_graph`](@ref Semigroups.current_word_graph) +""" +@cxxdereference word_graph(tc::ToddCoxeter) = LibSemigroups.word_graph(tc) + +# ============================================================================ +# Word <-> class index conversions +# ============================================================================ + +""" + index_of(tc::ToddCoxeter, w::AbstractVector{<:Integer}) -> Int + +Return the 1-based index of the congruence class containing `w`, triggering +a full enumeration of `tc`. + +If [`current_word_graph`](@ref Semigroups.current_word_graph) is not already +standardized, the algorithm first standardizes it with respect to short-lex +order; otherwise the existing standardization is preserved. Because +enumeration completes, the result is never +[`UNDEFINED`](@ref Semigroups.UNDEFINED). + +# Arguments + +- `tc::ToddCoxeter`: the `ToddCoxeter` instance. +- `w::AbstractVector{<:Integer}`: a 1-based `Vector{Int}` of letter + indices. + +# Throws + +- `LibsemigroupsError` if any letter in `w` is not in the alphabet of the + underlying presentation. + +!!! warning + This function may never terminate if the congruence is undecidable. + +# See also + +[`current_index_of`](@ref Semigroups.current_index_of), +[`word_of`](@ref Semigroups.word_of) +""" +function index_of(tc::ToddCoxeter, w::AbstractVector{<:Integer}) + cpp_w = _word_to_cpp(w) + cpp_i = @wrap_libsemigroups_call LibSemigroups.index_of(tc, cpp_w) + return _index_from_cpp(cpp_i) +end + +""" + current_index_of(tc::ToddCoxeter, w::AbstractVector{<:Integer}) -> Union{Int, UndefinedType} + +Return the 1-based index of the congruence class containing `w` if it is +already known, without triggering a run. + +If the class of `w` is not currently known, returns +[`UNDEFINED`](@ref Semigroups.UNDEFINED). Test for that case with +`result === UNDEFINED` (or [`is_undefined`](@ref Semigroups.is_undefined)); +`result == typemax(UInt)` will always be `false` because `UNDEFINED` is +its own singleton type. + +# Arguments + +- `tc::ToddCoxeter`: the `ToddCoxeter` instance. +- `w::AbstractVector{<:Integer}`: a 1-based `Vector{Int}` of letter + indices. + +# Throws + +- `LibsemigroupsError` if any letter in `w` is not in the alphabet of the + underlying presentation. + +# See also + +[`index_of`](@ref Semigroups.index_of), +[`current_word_of`](@ref Semigroups.current_word_of) +""" +function current_index_of(tc::ToddCoxeter, w::AbstractVector{<:Integer}) + cpp_w = _word_to_cpp(w) + cpp_i = @wrap_libsemigroups_call LibSemigroups.current_index_of(tc, cpp_w) + return _index_from_cpp(cpp_i) +end + +""" + word_of(tc::ToddCoxeter, i::Integer) -> Vector{Int} + +Return a representative word for the `i`-th congruence class of `tc` +(1-based). + +This function triggers a full enumeration of `tc`, which may never +terminate. + +# Arguments + +- `tc::ToddCoxeter`: the `ToddCoxeter` instance. +- `i::Integer`: a 1-based class index. + +# Throws + +- `LibsemigroupsError` if `i` is out of range. + +# See also + +[`current_word_of`](@ref Semigroups.current_word_of), +[`index_of`](@ref Semigroups.index_of) +""" +function word_of(tc::ToddCoxeter, i::Integer) + cpp_i = _index_to_cpp(i) + out = @wrap_libsemigroups_call LibSemigroups.word_of(tc, cpp_i) + return _word_from_cpp(out) +end + +""" + current_word_of(tc::ToddCoxeter, i::Integer) -> Vector{Int} + +Return a representative word for the `i`-th congruence class of `tc` +(1-based), without triggering an enumeration. + +If [`current_word_graph`](@ref Semigroups.current_word_graph) is not already +standardized, this function standardizes it with respect to short-lex order +as a side effect. The returned word is obtained by walking the +current-spanning-tree path from the node corresponding to class `i` back to +the root. + +# Arguments + +- `tc::ToddCoxeter`: the `ToddCoxeter` instance. +- `i::Integer`: a 1-based class index. + +# Throws + +- `LibsemigroupsError` if `i` is out of range relative to the current + state of `tc`. + +# See also + +[`word_of`](@ref Semigroups.word_of), +[`current_index_of`](@ref Semigroups.current_index_of) +""" +function current_word_of(tc::ToddCoxeter, i::Integer) + cpp_i = _index_to_cpp(i) + out = @wrap_libsemigroups_call LibSemigroups.current_word_of(tc, cpp_i) + return _word_from_cpp(out) +end + +""" + throw_if_letter_not_in_alphabet(tc::ToddCoxeter, w::AbstractVector{<:Integer}) + +Check that every letter in `w` belongs to the alphabet of the underlying +presentation of `tc`. + +# Arguments + +- `tc::ToddCoxeter`: the `ToddCoxeter` instance. +- `w::AbstractVector{<:Integer}`: a 1-based `Vector{Int}` of letter indices. + +# Throws + +- `LibsemigroupsError` if any letter in `w` is not in the alphabet of the + underlying presentation. +""" +function throw_if_letter_not_in_alphabet(tc::ToddCoxeter, w::AbstractVector{<:Integer}) + cpp_w = _word_to_cpp(w) + @wrap_libsemigroups_call LibSemigroups.throw_if_letter_not_in_alphabet(tc, cpp_w) + return nothing +end + +# ============================================================================ +# Query methods +# ============================================================================ + +""" + number_of_classes(tc::ToddCoxeter) -> UInt64 + +Compute the number of congruence classes, triggering a full run if +needed. + +Returns [`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY) if the +congruence has infinitely many classes. + +The return type is `UInt64` (rather than `Int`, which is the project +convention elsewhere) because `POSITIVE_INFINITY` is encoded as +`typemax(UInt64)` on the wire and would not round-trip through `Int`. +Use [`is_positive_infinity`](@ref Semigroups.is_positive_infinity) to +detect the infinite case. + +!!! warning + This function may not terminate if the congruence has infinitely many + classes and the algorithm cannot detect this. +""" +number_of_classes(tc::ToddCoxeter) = LibSemigroups.number_of_classes(tc) + +""" + kind(tc::ToddCoxeter) -> congruence_kind + +Return the kind (1- or 2-sided) of the congruence represented by `tc`. +The result is either [`twosided`](@ref Semigroups.twosided) or +[`onesided`](@ref Semigroups.onesided); see +[`congruence_kind`](@ref Semigroups.congruence_kind). +""" +kind(tc::ToddCoxeter) = LibSemigroups.kind(tc) + +""" + number_of_generating_pairs(tc::ToddCoxeter) -> Int + +Return the number of generating pairs added to `tc`. + +This equals the length of [`generating_pairs`](@ref +Semigroups.generating_pairs). +""" +number_of_generating_pairs(tc::ToddCoxeter) = + Int(LibSemigroups.number_of_generating_pairs(tc)) + +""" + generating_pairs(tc::ToddCoxeter) -> Vector{Tuple{Vector{Int}, Vector{Int}}} + +Return the generating pairs of `tc` as a vector of 1-based word pairs. + +These are the pairs added via +[`add_generating_pair!`](@ref Semigroups.add_generating_pair!). Words are +returned as 1-based `Vector{Int}` letter indices. The length of the returned +vector equals +[`number_of_generating_pairs`](@ref Semigroups.number_of_generating_pairs). +""" +function generating_pairs(tc::ToddCoxeter) + flat = LibSemigroups.generating_pairs(tc) + result = Tuple{Vector{Int},Vector{Int}}[] + for i = 1:2:length(flat) + push!(result, (_word_from_cpp(flat[i]), _word_from_cpp(flat[i+1]))) + end + return result +end + +""" + presentation(tc::ToddCoxeter) -> Presentation + +Return a copy of the presentation used to construct `tc`. + +If `tc` was constructed or initialised from a +[`WordGraph`](@ref Semigroups.WordGraph) rather than a +[`Presentation`](@ref Semigroups.Presentation), the returned presentation is +empty. +""" +presentation(tc::ToddCoxeter) = LibSemigroups.presentation(tc) + +# ============================================================================ +# Base.* overloads +# ============================================================================ + +""" + Base.length(tc::ToddCoxeter) -> UInt64 + +Return the number of congruence classes. Equivalent to +[`number_of_classes`](@ref Semigroups.number_of_classes). +""" +Base.length(tc::ToddCoxeter) = number_of_classes(tc) + +""" + Base.show(io::IO, tc::ToddCoxeter) + +Print a human-readable representation of `tc`. +""" +function Base.show(io::IO, tc::ToddCoxeter) + print(io, LibSemigroups.to_human_readable_repr(tc)) +end + +""" + Base.copy(tc::ToddCoxeter) -> ToddCoxeter + +Create an independent copy of `tc`. +""" +Base.copy(tc::ToddCoxeter) = LibSemigroups.ToddCoxeterWord(tc) + +Base.deepcopy_internal(tc::ToddCoxeter, ::IdDict) = LibSemigroups.ToddCoxeterWord(tc) + +# ============================================================================ +# Free functions (todd_coxeter:: namespace) +# ============================================================================ + +""" + is_non_trivial(tc::ToddCoxeter; + tries::Integer = 10, + try_for::TimePeriod = Dates.Millisecond(100), + threshold::Real = 0.99) -> tril + +Heuristically check whether the congruence represented by `tc` is +non-trivial. + +Repeatedly runs the algorithm for at most `try_for` time, with a random +subset of the generating pairs removed, up to `tries` times. If the ratio +of the number of classes in the modified system to the number in `tc` is +at most `threshold`, the function returns [`tril_TRUE`](@ref +Semigroups.tril_TRUE) (the congruence is likely non-trivial). If after +`tries` attempts no such ratio is observed, returns +[`tril_FALSE`](@ref Semigroups.tril_FALSE). Returns +[`tril_unknown`](@ref Semigroups.tril_unknown) if the function cannot +decide. + +# Arguments + +- `tc::ToddCoxeter`: the `ToddCoxeter` instance to check. +- `tries::Integer`: number of attempts (default `10`). +- `try_for::TimePeriod`: time budget per attempt (default + `Millisecond(100)`). +- `threshold::Real`: ratio threshold (default `0.99`). + +!!! note + The libsemigroups helper takes `std::chrono::milliseconds` internally; + the C++ binding receives nanoseconds and casts down, truncating + sub-millisecond values to zero. The default of `Millisecond(100)` is + safely above this precision boundary. +""" +function is_non_trivial( + tc::ToddCoxeter; + tries::Integer = 10, + try_for::TimePeriod = Dates.Millisecond(100), + threshold::Real = 0.99, +) + # Coerce all arguments outside `@wrap_libsemigroups_call` so a Julia-side + # InexactError (e.g. negative `tries`) propagates as itself instead of + # being re-wrapped as `LibsemigroupsError` by the macro's broad catch. + ut = UInt(tries) + ns = Int64(Dates.value(convert(Nanosecond, try_for))) + th = Float32(threshold) + return @wrap_libsemigroups_call LibSemigroups.tc_is_non_trivial(tc, ut, ns, th) +end + +""" + tc_redundant_rule(p::Presentation, timeout::TimePeriod) -> Union{Int, Nothing} + +Find a redundant rule in `p` using the Todd-Coxeter algorithm, with the +given timeout. + +Starting with the last rule in `p`, this function attempts to run +Todd-Coxeter on the rules of `p` with each rule omitted in turn. For each +omitted rule, Todd-Coxeter is run for at most `timeout`, then it checks +whether the omitted rule follows from the remaining rules. Returns the +1-based rule-pair index of the first redundant rule found, or `nothing` +if no redundant rule is identified within the timeout. + +!!! warning + This function is non-deterministic: results may differ between calls + with identical parameters. + +# Arguments + +- `p::Presentation`: the presentation to search. +- `timeout::TimePeriod`: maximum time per omitted rule (e.g. + `Millisecond(100)`, `Second(5)`). + +# See also + +[`redundant_rule`](@ref Semigroups.redundant_rule) - the analogous +Knuth-Bendix-based helper. +""" +function tc_redundant_rule(p::Presentation, timeout::TimePeriod) + # See note on is_non_trivial: keep coercions out of the macro arglist. + ns = Int64(Dates.value(convert(Nanosecond, timeout))) + idx = @wrap_libsemigroups_call LibSemigroups.tc_redundant_rule(p, ns) + n_flat = 2 * number_of_rules(p) + if idx >= n_flat + return nothing + end + # Convert from 0-based flat index (lhs position, always even) to + # 1-based rule-pair index. C++ contract guarantees lhs position. + return div(Int(idx), 2) + 1 +end diff --git a/test/runtests.jl b/test/runtests.jl index cf3bc55..7d4a1ee 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -26,4 +26,5 @@ using Semigroups include("test_froidure_pin_transf.jl") include("test_knuth_bendix_1.jl") include("test_knuth_bendix_6.jl") + include("test_todd_coxeter.jl") end diff --git a/test/test_todd_coxeter.jl b/test/test_todd_coxeter.jl new file mode 100644 index 0000000..77f7107 --- /dev/null +++ b/test/test_todd_coxeter.jl @@ -0,0 +1,367 @@ +# Copyright (c) 2026, James W. Swent +# +# Distributed under the terms of the GPL license version 3. + +""" +test_todd_coxeter.jl - Tests for ToddCoxeter + +Ports a focused subset of [quick] cases from +libsemigroups/tests/test-todd-coxeter.cpp, plus binding-surface and +high-level integration tests. +""" + +using Test +using Semigroups +using Dates + +# Build a 1-based Julia word from 0-based libsemigroups indices. +# `_tc_word(0, 0, 1)` -> `[1, 1, 2]`. Long prefix avoids shadowing in shared +# test scope. +_tc_word(xs::Integer...) = [Int(x) + 1 for x in xs] + +@testset "ToddCoxeter binding surface" begin + @test isdefined(Semigroups, :ToddCoxeter) + + # Constructors (4 forms) + @test hasmethod(ToddCoxeter, Tuple{congruence_kind,Presentation}) + @test hasmethod(ToddCoxeter, Tuple{congruence_kind,ToddCoxeter}) + @test hasmethod(ToddCoxeter, Tuple{congruence_kind,WordGraph}) + @test hasmethod(ToddCoxeter, Tuple{ToddCoxeter}) + + # init! overloads (4 forms) + @test hasmethod(init!, Tuple{ToddCoxeter}) + @test hasmethod(init!, Tuple{ToddCoxeter,congruence_kind,Presentation}) + @test hasmethod(init!, Tuple{ToddCoxeter,congruence_kind,ToddCoxeter}) + @test hasmethod(init!, Tuple{ToddCoxeter,congruence_kind,WordGraph}) + + # Settings (8 getter / 8 setter pairs) + @test hasmethod(strategy, Tuple{ToddCoxeter}) + @test hasmethod(strategy!, Tuple{ToddCoxeter,typeof(strategy_hlt)}) + @test hasmethod(lookahead_extent, Tuple{ToddCoxeter}) + @test hasmethod(lookahead_extent!, Tuple{ToddCoxeter,typeof(lookahead_extent_full)}) + @test hasmethod(lookahead_style, Tuple{ToddCoxeter}) + @test hasmethod(lookahead_style!, Tuple{ToddCoxeter,typeof(lookahead_style_hlt)}) + @test hasmethod(save, Tuple{ToddCoxeter}) + @test hasmethod(save!, Tuple{ToddCoxeter,Bool}) + @test hasmethod(use_relations_in_extra, Tuple{ToddCoxeter}) + @test hasmethod(use_relations_in_extra!, Tuple{ToddCoxeter,Bool}) + @test hasmethod(lower_bound, Tuple{ToddCoxeter}) + @test hasmethod(lower_bound!, Tuple{ToddCoxeter,Integer}) + @test hasmethod(def_version, Tuple{ToddCoxeter}) + @test hasmethod(def_version!, Tuple{ToddCoxeter,typeof(def_version_one)}) + @test hasmethod(def_policy, Tuple{ToddCoxeter}) + @test hasmethod(def_policy!, Tuple{ToddCoxeter,typeof(def_policy_purge_all)}) + + # Standardize and word-graph access + @test hasmethod(standardize!, Tuple{ToddCoxeter,Order}) + @test hasmethod(is_standardized, Tuple{ToddCoxeter}) + @test hasmethod(is_standardized, Tuple{ToddCoxeter,Order}) + @test hasmethod(current_word_graph, Tuple{ToddCoxeter}) + @test hasmethod(word_graph, Tuple{ToddCoxeter}) + + # Word <-> class index + @test hasmethod(index_of, Tuple{ToddCoxeter,AbstractVector{<:Integer}}) + @test hasmethod(current_index_of, Tuple{ToddCoxeter,AbstractVector{<:Integer}}) + @test hasmethod(word_of, Tuple{ToddCoxeter,Integer}) + @test hasmethod(current_word_of, Tuple{ToddCoxeter,Integer}) + @test hasmethod( + throw_if_letter_not_in_alphabet, + Tuple{ToddCoxeter,AbstractVector{<:Integer}}, + ) + + # Query methods + @test hasmethod(number_of_classes, Tuple{ToddCoxeter}) + @test hasmethod(kind, Tuple{ToddCoxeter}) + @test hasmethod(number_of_generating_pairs, Tuple{ToddCoxeter}) + @test hasmethod(generating_pairs, Tuple{ToddCoxeter}) + @test hasmethod(presentation, Tuple{ToddCoxeter}) + + # Free functions + @test hasmethod(is_non_trivial, Tuple{ToddCoxeter}) + @test hasmethod(tc_redundant_rule, Tuple{Presentation,TimePeriod}) + + # Base.* overloads + @test hasmethod(Base.length, Tuple{ToddCoxeter}) + @test hasmethod(Base.show, Tuple{IO,ToddCoxeter}) + @test hasmethod(Base.copy, Tuple{ToddCoxeter}) + + # Inherited from CongruenceCommon (wrapped in src/cong-common.jl). + # `contains` and `reduce` shadow Base, so they stay module-qualified. + @test hasmethod( + add_generating_pair!, + Tuple{CongruenceCommon,AbstractVector{<:Integer},AbstractVector{<:Integer}}, + ) + @test hasmethod( + currently_contains, + Tuple{CongruenceCommon,AbstractVector{<:Integer},AbstractVector{<:Integer}}, + ) + @test hasmethod( + Semigroups.contains, + Tuple{CongruenceCommon,AbstractVector{<:Integer},AbstractVector{<:Integer}}, + ) + @test hasmethod(Semigroups.reduce, Tuple{CongruenceCommon,AbstractVector{<:Integer}}) + @test hasmethod(reduce_no_run, Tuple{CongruenceCommon,AbstractVector{<:Integer}}) + @test hasmethod(normal_forms, Tuple{CongruenceCommon}) + @test hasmethod(non_trivial_classes, Tuple{CongruenceCommon,CongruenceCommon}) +end + +# ============================================================================ +# correctness tests inspired by test-todd-coxeter.cpp +# ============================================================================ + +@testset "TC000 - small 2-sided congruence (27 classes)" begin + # Port of libsemigroups TC000 (test-todd-coxeter.cpp:294-341). + # 2-generator semigroup, rules: 000 = 0, 1111 = 1, 0101 = 00. + p = Presentation() + set_alphabet!(p, 2) + add_rule_no_checks!(p, _tc_word(0, 0, 0), _tc_word(0)) + add_rule_no_checks!(p, _tc_word(1, 1, 1, 1), _tc_word(1)) + add_rule_no_checks!(p, _tc_word(0, 1, 0, 1), _tc_word(0, 0)) + + tc = ToddCoxeter(twosided, p) + @test number_of_classes(tc) == 27 + @test finished(tc) + + standardize!(tc, ORDER_SHORTLEX) + @test length(normal_forms(tc)) == 27 +end + +@testset "TC001 - small 2-sided congruence (5 classes)" begin + # Port of libsemigroups TC001 (test-todd-coxeter.cpp:343-441). + # 2-generator semigroup, rules: 000 = 0, 0 = 11. + p = Presentation() + set_alphabet!(p, 2) + add_rule_no_checks!(p, _tc_word(0, 0, 0), _tc_word(0)) + add_rule_no_checks!(p, _tc_word(0), _tc_word(1, 1)) + + tc = ToddCoxeter(twosided, p) + run!(tc) + @test number_of_classes(tc) == 5 + @test finished(tc) + + # Index-of: 001 == 00001 (1-based: [1,1,2] == [1,1,1,1,2]) + @test index_of(tc, _tc_word(0, 0, 1)) == index_of(tc, _tc_word(0, 0, 0, 0, 1)) + @test index_of(tc, _tc_word(0, 1, 1, 0, 0, 1)) == index_of(tc, _tc_word(0, 0, 0, 0, 1)) + @test index_of(tc, _tc_word(0, 0, 0)) != index_of(tc, _tc_word(1)) + + # Standardize for shortlex (TC001 lines 371-374) + standardize!(tc, ORDER_SHORTLEX) + @test word_of(tc, 1) == _tc_word(0) # C++ index 0 + @test word_of(tc, 2) == _tc_word(1) # C++ index 1 + @test word_of(tc, 3) == _tc_word(0, 0) # C++ index 2 + + # Standardize for lex (TC001 lines 375-391) + standardize!(tc, ORDER_LEX) + @test is_standardized(tc, ORDER_LEX) + @test is_standardized(tc) + @test !is_standardized(tc, ORDER_SHORTLEX) + + @test word_of(tc, 1) == _tc_word(0) # 0 + @test word_of(tc, 2) == _tc_word(0, 0) # 00 + @test word_of(tc, 3) == _tc_word(0, 0, 1) # 001 + @test word_of(tc, 4) == _tc_word(0, 0, 1, 0) # 0010 + @test word_of(tc, 5) == _tc_word(1) # 1 + + # word_of/index_of round-trip (1-based) + for i = 1:5 + @test index_of(tc, word_of(tc, i)) == i + end + + standardize!(tc, ORDER_SHORTLEX) + @test is_standardized(tc, ORDER_SHORTLEX) + @test normal_forms(tc) == + [_tc_word(0), _tc_word(1), _tc_word(0, 0), _tc_word(0, 1), _tc_word(0, 0, 1)] +end + +@testset "TC - quotient construction (kind, ToddCoxeter)" begin + # Port of libsemigroups TC025 (test-todd-coxeter.cpp:1447-1468), reduced. + p = Presentation() + set_alphabet!(p, 2) + add_rule_no_checks!(p, _tc_word(0, 0, 0), _tc_word(0)) + add_rule_no_checks!(p, _tc_word(0), _tc_word(1, 1)) + + tc1 = ToddCoxeter(twosided, p) + @test number_of_classes(tc1) == 5 + + tc2 = ToddCoxeter(onesided, tc1) + add_generating_pair!(tc2, _tc_word(0), _tc_word(0, 0)) + @test number_of_classes(tc2) == 3 +end + +@testset "TC024 - constructor from WordGraph" begin + # Port of libsemigroups TC024 (test-todd-coxeter.cpp:1435-1445). + # Upstream only requires the constructor not to throw; mirror that and add + # a few cheap state observations. The 1-node, 2-out-degree WordGraph has + # all-UNDEFINED targets. + wg = WordGraph(1, 2) + @test out_degree(wg) == 2 + @test number_of_nodes(wg) == 1 + + tc = ToddCoxeter(twosided, wg) + @test tc isa ToddCoxeter + @test kind(tc) == twosided + @test (finished(tc); started(tc); true) +end + +@testset "TC settings round-trip (8 pairs)" begin + p = Presentation() + set_alphabet!(p, 2) + add_rule_no_checks!(p, _tc_word(0, 0, 0), _tc_word(0)) + add_rule_no_checks!(p, _tc_word(0), _tc_word(1, 1)) + tc = ToddCoxeter(twosided, p) + + strategy!(tc, strategy_felsch) + @test strategy(tc) == strategy_felsch + + lookahead_extent!(tc, lookahead_extent_full) + @test lookahead_extent(tc) == lookahead_extent_full + + lookahead_style!(tc, lookahead_style_felsch) + @test lookahead_style(tc) == lookahead_style_felsch + + save!(tc, true) + @test save(tc) == true + + use_relations_in_extra!(tc, false) + @test use_relations_in_extra(tc) == false + + # Default is UNDEFINED (no bound). Setting then clearing round-trips. + @test lower_bound(tc) === UNDEFINED + lower_bound!(tc, 5) + @test lower_bound(tc) == 5 + lower_bound!(tc, UNDEFINED) + @test lower_bound(tc) === UNDEFINED + + def_version!(tc, def_version_two) + @test def_version(tc) == def_version_two + + def_policy!(tc, def_policy_purge_all) + @test def_policy(tc) == def_policy_purge_all +end + +@testset "TC - current_word_graph after run!" begin + # After run!, current_word_graph may include inactive allocation slots, + # so it has at least number_of_classes(tc) + 1 nodes (the +1 accounts for + # the inactive node 0 for presentations without the empty word). + # word_graph(tc) standardizes and prunes, giving exactly that count. + p = Presentation() + set_alphabet!(p, 2) + add_rule_no_checks!(p, _tc_word(0, 0, 0), _tc_word(0)) + add_rule_no_checks!(p, _tc_word(0), _tc_word(1, 1)) + + tc = ToddCoxeter(twosided, p) + run!(tc) + + cwg = current_word_graph(tc) + @test number_of_nodes(cwg) >= number_of_classes(tc) + 1 + + swg = word_graph(tc) + @test number_of_nodes(swg) == number_of_classes(tc) + 1 +end + +@testset "TC - is_non_trivial on free monogenic" begin + # Port of libsemigroups TC031 fragment (test-todd-coxeter.cpp:1730-1739). + # Free monogenic semigroup is non-trivial. + p = Presentation() + set_alphabet!(p, 1) + tc = ToddCoxeter(twosided, p) + @test is_non_trivial(tc) == tril_TRUE +end + +@testset "TC - tc_redundant_rule" begin + p = Presentation() + set_alphabet!(p, 2) + add_rule_no_checks!(p, _tc_word(0, 0, 0), _tc_word(0)) + add_rule_no_checks!(p, _tc_word(0), _tc_word(1, 1)) + @test tc_redundant_rule(p, Millisecond(50)) === nothing + + # Add a duplicate rule -> trivially redundant. + add_rule_no_checks!(p, _tc_word(0, 0, 0), _tc_word(0)) + idx = tc_redundant_rule(p, Millisecond(100)) + @test idx isa Integer + @test 1 <= idx <= number_of_rules(p) +end + +@testset "TC - throw_if_letter_not_in_alphabet" begin + p = Presentation() + set_alphabet!(p, 2) + add_rule_no_checks!(p, _tc_word(0, 0, 0), _tc_word(0)) + add_rule_no_checks!(p, _tc_word(0), _tc_word(1, 1)) + tc = ToddCoxeter(twosided, p) + + @test throw_if_letter_not_in_alphabet(tc, _tc_word(0, 1, 0)) === nothing + @test_throws LibsemigroupsError throw_if_letter_not_in_alphabet(tc, _tc_word(0, 5)) +end + +@testset "TC - cong-common helpers (reduce, contains, currently_contains, normal_forms)" begin + p = Presentation() + set_alphabet!(p, 2) + add_rule_no_checks!(p, _tc_word(0, 0, 0), _tc_word(0)) + add_rule_no_checks!(p, _tc_word(0), _tc_word(1, 1)) + tc = ToddCoxeter(twosided, p) + + # contains triggers a run and agrees with index_of equality. + @test Semigroups.contains(tc, _tc_word(0, 0, 1), _tc_word(0, 0, 0, 0, 1)) + @test currently_contains(tc, _tc_word(0, 0, 1), _tc_word(0, 0, 0, 0, 1)) == tril_TRUE + + r = Semigroups.reduce(tc, _tc_word(0, 0, 0, 0, 1)) + @test r isa Vector{Int} + @test Semigroups.contains(tc, r, _tc_word(0, 0, 0, 0, 1)) + + @test length(normal_forms(tc)) == number_of_classes(tc) +end + +@testset "TC - non_trivial_classes(tc1, tc2) for a quotient pair" begin + p = Presentation() + set_alphabet!(p, 2) + add_rule_no_checks!(p, _tc_word(0, 0, 0), _tc_word(0)) + add_rule_no_checks!(p, _tc_word(0), _tc_word(1, 1)) + + tc1 = ToddCoxeter(twosided, p) + @test number_of_classes(tc1) == 5 + + # tc2 collapses 0 ~ 1 -> strictly fewer classes than tc1. + tc2 = ToddCoxeter(twosided, p) + add_generating_pair!(tc2, _tc_word(0), _tc_word(1)) + @test number_of_classes(tc2) < number_of_classes(tc1) + + classes = non_trivial_classes(tc1, tc2) + @test classes isa AbstractVector +end + +@testset "ToddCoxeter high-level Julia API" begin + p = Presentation() + set_alphabet!(p, 2) + add_rule_no_checks!(p, _tc_word(0, 0, 0), _tc_word(0)) + add_rule_no_checks!(p, _tc_word(0), _tc_word(1, 1)) + + tc = ToddCoxeter(twosided, p) + @test length(tc) == number_of_classes(tc) + @test !isempty(sprint(show, tc)) + + # copy: independent objects. + tc2 = copy(tc) + @test length(tc2) == length(tc) + strategy!(tc2, strategy_felsch) + @test strategy(tc2) == strategy_felsch + + # 1-based round-trip for index_of / word_of. + standardize!(tc, ORDER_SHORTLEX) + for i = 1:Int(number_of_classes(tc)) + @test index_of(tc, word_of(tc, i)) == i + end + + # Setter chaining: each setter returns tc, so calls compose left-to-right. + save!(strategy!(tc, strategy_felsch), false) + @test strategy(tc) == strategy_felsch + @test save(tc) == false + + # standardize! returns a Bool. + fresh = ToddCoxeter(twosided, p) + run!(fresh) + @test standardize!(fresh, ORDER_SHORTLEX) isa Bool +end + +# ============================================================================ +# TODO - port full test-todd-coxeter.cpp test cases +# ============================================================================