From 442a9004a6b19a4aac410c101f1dc71c16d2782c Mon Sep 17 00:00:00 2001 From: James Swent Date: Sun, 26 Apr 2026 19:20:39 +0100 Subject: [PATCH 01/17] feat: add ToddCoxeter bindings --- deps/src/CMakeLists.txt | 1 + deps/src/cong-common.cpp | 107 +-------- deps/src/cong-common.hpp | 140 ++++++++++++ deps/src/libsemigroups_julia.cpp | 2 + deps/src/libsemigroups_julia.hpp | 2 + deps/src/todd-coxeter.cpp | 359 +++++++++++++++++++++++++++++++ 6 files changed, 508 insertions(+), 103 deletions(-) create mode 100644 deps/src/cong-common.hpp create mode 100644 deps/src/todd-coxeter.cpp diff --git a/deps/src/CMakeLists.txt b/deps/src/CMakeLists.txt index 20b51a5..0a8ead7 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 diff --git a/deps/src/cong-common.cpp b/deps/src/cong-common.cpp index 2352da4..28e7341 100644 --- a/deps/src/cong-common.cpp +++ b/deps/src/cong-common.cpp @@ -18,18 +18,17 @@ // CRITICAL: libsemigroups_julia.hpp MUST be included first (fmt consteval fix) #include "libsemigroups_julia.hpp" -#include +// libsemigroups headers that provide algorithm-specific overloads of +// congruence_common helpers MUST be included BEFORE cong-common.hpp so the +// template bodies in cong-common.hpp pick them up at instantiation. #include #include #include #include -#include +#include "cong-common.hpp" -#include -#include #include -#include namespace jlcxx { template <> @@ -44,104 +43,6 @@ 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; diff --git a/deps/src/cong-common.hpp b/deps/src/cong-common.hpp new file mode 100644 index 0000000..b943457 --- /dev/null +++ b/deps/src/cong-common.hpp @@ -0,0 +1,140 @@ +// +// 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. + +#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); + }); + } + +} // namespace libsemigroups_julia + +#endif // LIBSEMIGROUPS_JULIA_CONG_COMMON_HPP_ diff --git a/deps/src/libsemigroups_julia.cpp b/deps/src/libsemigroups_julia.cpp index 7eab2d7..ab72407 100644 --- a/deps/src/libsemigroups_julia.cpp +++ b/deps/src/libsemigroups_julia.cpp @@ -54,7 +54,9 @@ namespace libsemigroups_julia { define_presentation(mod); define_presentation_examples(mod); define_knuth_bendix(mod); + define_todd_coxeter(mod); define_knuth_bendix_cong_common_helpers(mod); + define_todd_coxeter_cong_common_helpers(mod); } } // namespace libsemigroups_julia diff --git a/deps/src/libsemigroups_julia.hpp b/deps/src/libsemigroups_julia.hpp index 026e99c..8980da3 100644 --- a/deps/src/libsemigroups_julia.hpp +++ b/deps/src/libsemigroups_julia.hpp @@ -70,6 +70,8 @@ namespace libsemigroups_julia { 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); + void define_todd_coxeter_cong_common_helpers(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..137820b --- /dev/null +++ b/deps/src/todd-coxeter.cpp @@ -0,0 +1,359 @@ +// +// 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; + + //////////////////////////////////////////////////////////////////////// + // Enums + //////////////////////////////////////////////////////////////////////// + + // 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 (lives on FelschGraphSettings::options, inherited via + // TCImpl::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); }); + + //////////////////////////////////////////////////////////////////////// + // Settings (getter / setter pairs with DISTINCT names) + //////////////////////////////////////////////////////////////////////// + + // 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); + }); + + //////////////////////////////////////////////////////////////////////// + // Standardize / word-graph access + //////////////////////////////////////////////////////////////////////// + + type.method("standardize!", [](TC& self, Order ord) -> bool { + return self.standardize(ord); + }); + + type.method("is_standardized", + [](TC const& self) -> bool { return self.is_standardized(); }); + + type.method("is_standardized", [](TC const& self, Order ord) -> bool { + return self.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(); + }); + + //////////////////////////////////////////////////////////////////////// + // Word <-> class index + //////////////////////////////////////////////////////////////////////// + + 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("word_of", [](TC& self, size_t i) -> word_type { + word_type out; + self.word_of(std::back_inserter(out), i); + return out; + }); + + //////////////////////////////////////////////////////////////////////// + // Query methods + //////////////////////////////////////////////////////////////////////// + + 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(); + }); + + //////////////////////////////////////////////////////////////////////// + // Display + //////////////////////////////////////////////////////////////////////// + + type.method("to_human_readable_repr", [](TC const& self) -> std::string { + return libsemigroups::to_human_readable_repr(self); + }); + + //////////////////////////////////////////////////////////////////////// + // Free functions (todd_coxeter:: namespace) + //////////////////////////////////////////////////////////////////////// + + // 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::milliseconds>( + 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)); + }); + } + + void define_todd_coxeter_cong_common_helpers(jl::Module& m) { + using libsemigroups::word_type; + using TC = libsemigroups::ToddCoxeter; + + define_cong_common_word_helpers(m); + define_cong_common_normal_forms(m); + define_cong_common_non_trivial_classes(m); + } + +} // namespace libsemigroups_julia From 5c2d3faa0123990685857750ba808820bc0fc617 Mon Sep 17 00:00:00 2001 From: James Swent Date: Sun, 26 Apr 2026 19:42:36 +0100 Subject: [PATCH 02/17] chore: clarify cong-common.hpp include-order requirement --- deps/src/cong-common.hpp | 13 +++++++++++++ deps/src/todd-coxeter.cpp | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/deps/src/cong-common.hpp b/deps/src/cong-common.hpp index b943457..0bdf9b5 100644 --- a/deps/src/cong-common.hpp +++ b/deps/src/cong-common.hpp @@ -23,6 +23,19 @@ // 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_ diff --git a/deps/src/todd-coxeter.cpp b/deps/src/todd-coxeter.cpp index 137820b..0685379 100644 --- a/deps/src/todd-coxeter.cpp +++ b/deps/src/todd-coxeter.cpp @@ -117,8 +117,8 @@ namespace libsemigroups_julia { m.set_const("def_policy_unlimited", TCImpl::options::def_policy::unlimited); - // def_version (lives on FelschGraphSettings::options, inherited via - // TCImpl::options) + // 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); From ceed6d61224348dd049c2a42ba4abbfc6457a150 Mon Sep 17 00:00:00 2001 From: James Swent Date: Sun, 26 Apr 2026 19:55:31 +0100 Subject: [PATCH 03/17] feat: add ToddCoxeter Julia wrapper skeleton --- src/Semigroups.jl | 1 + src/todd-coxeter.jl | 213 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 src/todd-coxeter.jl diff --git a/src/Semigroups.jl b/src/Semigroups.jl index 7a70265..11be162 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") diff --git a/src/todd-coxeter.jl b/src/todd-coxeter.jl new file mode 100644 index 0000000..28223ca --- /dev/null +++ b/src/todd-coxeter.jl @@ -0,0 +1,213 @@ +# 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). + +!!! 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: HLT (Hazelgrove-Leech-Trotter) coset enumeration. +See [`strategy!`](@ref Semigroups.strategy!). +""" +const strategy_hlt = LibSemigroups.strategy_hlt + +""" + strategy_felsch + +Strategy enum value: Felsch-style coset enumeration. Definitions are made +greedily, applying the relations of the presentation immediately to identify +classes. See [`strategy!`](@ref Semigroups.strategy!). +""" +const strategy_felsch = LibSemigroups.strategy_felsch + +""" + strategy_CR + +Strategy enum value: alternating between HLT (Cosets) and Felsch (Relations). +See Holt, Eick, O'Brien, *Handbook of Computational Group Theory*, §5.3. +See [`strategy!`](@ref Semigroups.strategy!). +""" +const strategy_CR = LibSemigroups.strategy_CR + +""" + strategy_R_over_C + +Strategy enum value: HLT phase, followed by Felsch phase. +See [`strategy!`](@ref Semigroups.strategy!). +""" +const strategy_R_over_C = LibSemigroups.strategy_R_over_C + +""" + strategy_Cr + +Strategy enum value: short Felsch phase, followed by HLT, with one final +Felsch sweep at the end. See [`strategy!`](@ref Semigroups.strategy!). +""" +const strategy_Cr = LibSemigroups.strategy_Cr + +""" + strategy_Rc + +Strategy enum value: short HLT phase, followed by Felsch, with one final +HLT sweep at the end. See [`strategy!`](@ref Semigroups.strategy!). +""" +const strategy_Rc = LibSemigroups.strategy_Rc + +# --- lookahead_extent ------------------------------------------------------- + +""" + lookahead_extent_full + +Lookahead-extent enum value: lookahead processes the full word graph. +See [`lookahead_extent!`](@ref Semigroups.lookahead_extent!). +""" +const lookahead_extent_full = LibSemigroups.lookahead_extent_full + +""" + lookahead_extent_partial + +Lookahead-extent enum value: lookahead processes only part of the word +graph. 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. +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. +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: do not stack a deduction if there is no space +left. 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: purge from the top of the deduction stack +when full. 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: purge all deductions when the stack is full. +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: discard all deductions if there is no space +left. 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: do not limit the number of stacked deductions. +See [`def_policy!`](@ref Semigroups.def_policy!). +""" +const def_policy_unlimited = LibSemigroups.def_policy_unlimited + +# --- def_version ------------------------------------------------------------ + +""" + def_version_one + +Definition-version enum value: version 1 of the definition routine. +See [`def_version!`](@ref Semigroups.def_version!). +""" +const def_version_one = LibSemigroups.def_version_one + +""" + def_version_two + +Definition-version enum value: version 2 of the definition routine. +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 From a06ab18b011f34f2d0c0ee8c352dd6e671d68e03 Mon Sep 17 00:00:00 2001 From: James Swent Date: Sun, 26 Apr 2026 19:57:38 +0100 Subject: [PATCH 04/17] test: add ToddCoxeter test file Ports a focused subset of [quick] cases from libsemigroups/tests/test-todd-coxeter.cpp --- test/runtests.jl | 1 + test/test_todd_coxeter.jl | 398 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 399 insertions(+) create mode 100644 test/test_todd_coxeter.jl 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..c9130e2 --- /dev/null +++ b/test/test_todd_coxeter.jl @@ -0,0 +1,398 @@ +# Copyright (c) 2026, James W. Swent +# +# Distributed under the terms of the GPL license version 3. + +""" +test_todd_coxeter.jl - Tests for ToddCoxeter + +Phase 3b of the v1 design. Ports a focused subset of [quick] cases from +libsemigroups/tests/test-todd-coxeter.cpp, plus binding-surface and high-level +integration tests. + +This file is brought in **before** the Julia wrapper for ToddCoxeter is fully +implemented — by design. Many assertions will fail with `MethodError` or +`UndefVarError` until Stage 3 implements the wrapper methods one slice at a +time. The constructor and `number_of_classes` work directly off the C++ glue +and so a handful of assertions may already pass. +""" + +using Test +using Semigroups +using Dates + +# ToddCoxeter and the enum constants are not yet exported from `Semigroups` +# (Stage 4 adds the exports). Bring them into local scope under their public +# names for readability. +const ToddCoxeter = Semigroups.ToddCoxeter + +const strategy_hlt = Semigroups.strategy_hlt +const strategy_felsch = Semigroups.strategy_felsch +const strategy_CR = Semigroups.strategy_CR +const strategy_R_over_C = Semigroups.strategy_R_over_C +const strategy_Cr = Semigroups.strategy_Cr +const strategy_Rc = Semigroups.strategy_Rc + +const lookahead_extent_full = Semigroups.lookahead_extent_full +const lookahead_extent_partial = Semigroups.lookahead_extent_partial + +const lookahead_style_hlt = Semigroups.lookahead_style_hlt +const lookahead_style_felsch = Semigroups.lookahead_style_felsch + +const def_policy_no_stack_if_no_space = Semigroups.def_policy_no_stack_if_no_space +const def_policy_purge_from_top = Semigroups.def_policy_purge_from_top +const def_policy_purge_all = Semigroups.def_policy_purge_all +const def_policy_discard_all_if_no_space = Semigroups.def_policy_discard_all_if_no_space +const def_policy_unlimited = Semigroups.def_policy_unlimited + +const def_version_one = Semigroups.def_version_one +const def_version_two = Semigroups.def_version_two + +# ---- Word conversion helpers (1-based Julia <-> 0-based C++ in libsemigroups) ---- + +# Mirror the KB pattern: `_tc_cword(0, 0, 1)` builds the Julia word `[1, 1, 2]`. +_tc_cword(xs::Integer...) = [Int(x) + 1 for x in xs] + +# ============================================================================ +# Layer 1 — binding-surface tests +# ============================================================================ + +@testset "ToddCoxeter binding surface" begin + @test isdefined(Semigroups, :ToddCoxeter) + + # Constructors (4 forms) + @test hasmethod(ToddCoxeter, Tuple{Semigroups.congruence_kind, Presentation}) + @test hasmethod(ToddCoxeter, Tuple{Semigroups.congruence_kind, ToddCoxeter}) + @test hasmethod(ToddCoxeter, Tuple{Semigroups.congruence_kind, WordGraph}) + @test hasmethod(ToddCoxeter, Tuple{ToddCoxeter}) + + # init! overloads (4 forms) + @test hasmethod(init!, Tuple{ToddCoxeter}) + @test hasmethod(init!, Tuple{ToddCoxeter, Semigroups.congruence_kind, Presentation}) + @test hasmethod(init!, Tuple{ToddCoxeter, Semigroups.congruence_kind, ToddCoxeter}) + @test hasmethod(init!, Tuple{ToddCoxeter, Semigroups.congruence_kind, WordGraph}) + + # Settings (8 getter / 8 setter pairs) + @test hasmethod(Semigroups.strategy, Tuple{ToddCoxeter}) + @test hasmethod(Semigroups.strategy!, Tuple{ToddCoxeter, Any}) + @test hasmethod(Semigroups.lookahead_extent, Tuple{ToddCoxeter}) + @test hasmethod(Semigroups.lookahead_extent!, Tuple{ToddCoxeter, Any}) + @test hasmethod(Semigroups.lookahead_style, Tuple{ToddCoxeter}) + @test hasmethod(Semigroups.lookahead_style!, Tuple{ToddCoxeter, Any}) + @test hasmethod(Semigroups.save, Tuple{ToddCoxeter}) + @test hasmethod(Semigroups.save!, Tuple{ToddCoxeter, Bool}) + @test hasmethod(Semigroups.use_relations_in_extra, Tuple{ToddCoxeter}) + @test hasmethod(Semigroups.use_relations_in_extra!, Tuple{ToddCoxeter, Bool}) + @test hasmethod(Semigroups.lower_bound, Tuple{ToddCoxeter}) + @test hasmethod(Semigroups.lower_bound!, Tuple{ToddCoxeter, Integer}) + @test hasmethod(Semigroups.def_version, Tuple{ToddCoxeter}) + @test hasmethod(Semigroups.def_version!, Tuple{ToddCoxeter, Any}) + @test hasmethod(Semigroups.def_policy, Tuple{ToddCoxeter}) + @test hasmethod(Semigroups.def_policy!, Tuple{ToddCoxeter, Any}) + + # Standardize and word-graph access + @test hasmethod(standardize!, Tuple{ToddCoxeter, Order}) + @test hasmethod(Semigroups.is_standardized, Tuple{ToddCoxeter}) + @test hasmethod(Semigroups.is_standardized, Tuple{ToddCoxeter, Order}) + @test hasmethod(Semigroups.current_word_graph, Tuple{ToddCoxeter}) + @test hasmethod(word_graph, Tuple{ToddCoxeter}) + + # Word <-> class index + @test hasmethod(Semigroups.index_of, Tuple{ToddCoxeter, AbstractVector{<:Integer}}) + @test hasmethod(Semigroups.current_index_of, Tuple{ToddCoxeter, AbstractVector{<:Integer}}) + @test hasmethod(Semigroups.word_of, Tuple{ToddCoxeter, Integer}) + @test hasmethod(Semigroups.current_word_of, Tuple{ToddCoxeter, 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(Semigroups.is_non_trivial, Tuple{ToddCoxeter}) + @test hasmethod(Semigroups.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 (already wrapped in src/cong-common.jl) + @test hasmethod(add_generating_pair!, Tuple{Semigroups.CongruenceCommon, AbstractVector{<:Integer}, AbstractVector{<:Integer}}) + @test hasmethod(currently_contains, Tuple{Semigroups.CongruenceCommon, AbstractVector{<:Integer}, AbstractVector{<:Integer}}) + @test hasmethod(contains, Tuple{Semigroups.CongruenceCommon, AbstractVector{<:Integer}, AbstractVector{<:Integer}}) + @test hasmethod(Semigroups.reduce, Tuple{Semigroups.CongruenceCommon, AbstractVector{<:Integer}}) + @test hasmethod(reduce_no_run, Tuple{Semigroups.CongruenceCommon, AbstractVector{<:Integer}}) + @test hasmethod(normal_forms, Tuple{Semigroups.CongruenceCommon}) + @test hasmethod(non_trivial_classes, Tuple{Semigroups.CongruenceCommon, Semigroups.CongruenceCommon}) +end + +# ============================================================================ +# Layer 2 — correctness (ported from 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_cword(0, 0, 0), _tc_cword(0)) + add_rule_no_checks!(p, _tc_cword(1, 1, 1, 1), _tc_cword(1)) + add_rule_no_checks!(p, _tc_cword(0, 1, 0, 1), _tc_cword(0, 0)) + + tc = ToddCoxeter(twosided, p) + @test number_of_classes(tc) == 27 + @test finished(tc) + + # standardize + normal_forms count matches number_of_classes + standardize!(tc, ORDER_SHORTLEX) + nfs = normal_forms(tc) + @test length(nfs) == 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_cword(0, 0, 0), _tc_cword(0)) + add_rule_no_checks!(p, _tc_cword(0), _tc_cword(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 Semigroups.index_of(tc, _tc_cword(0, 0, 1)) == + Semigroups.index_of(tc, _tc_cword(0, 0, 0, 0, 1)) + @test Semigroups.index_of(tc, _tc_cword(0, 1, 1, 0, 0, 1)) == + Semigroups.index_of(tc, _tc_cword(0, 0, 0, 0, 1)) + @test Semigroups.index_of(tc, _tc_cword(0, 0, 0)) != + Semigroups.index_of(tc, _tc_cword(1)) + + # Standardize for shortlex (TC001 lines 371-374) + standardize!(tc, ORDER_SHORTLEX) + @test Semigroups.word_of(tc, 1) == _tc_cword(0) # C++ index 0 + @test Semigroups.word_of(tc, 2) == _tc_cword(1) # C++ index 1 + @test Semigroups.word_of(tc, 3) == _tc_cword(0, 0) # C++ index 2 + + # Standardize for lex (TC001 lines 375-391) + standardize!(tc, ORDER_LEX) + @test Semigroups.is_standardized(tc, ORDER_LEX) + @test Semigroups.is_standardized(tc) + @test !Semigroups.is_standardized(tc, ORDER_SHORTLEX) + + @test Semigroups.word_of(tc, 1) == _tc_cword(0) # 0 + @test Semigroups.word_of(tc, 2) == _tc_cword(0, 0) # 00 + @test Semigroups.word_of(tc, 3) == _tc_cword(0, 0, 1) # 001 + @test Semigroups.word_of(tc, 4) == _tc_cword(0, 0, 1, 0) # 0010 + @test Semigroups.word_of(tc, 5) == _tc_cword(1) # 1 + + # word_of/index_of round-trip (1-based on the Julia side) + for i in 1:5 + @test Semigroups.index_of(tc, Semigroups.word_of(tc, i)) == i + end + + # Standardize for shortlex again, and check normal_forms equals expected. + standardize!(tc, ORDER_SHORTLEX) + @test Semigroups.is_standardized(tc, ORDER_SHORTLEX) + @test normal_forms(tc) == [ + _tc_cword(0), + _tc_cword(1), + _tc_cword(0, 0), + _tc_cword(0, 1), + _tc_cword(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_cword(0, 0, 0), _tc_cword(0)) + add_rule_no_checks!(p, _tc_cword(0), _tc_cword(1, 1)) + + tc1 = ToddCoxeter(twosided, p) + @test number_of_classes(tc1) == 5 + + tc2 = ToddCoxeter(onesided, tc1) + add_generating_pair!(tc2, _tc_cword(0), _tc_cword(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). + wg = WordGraph(1, 2) + @test out_degree(wg) == 2 + @test number_of_nodes(wg) == 1 + tc = ToddCoxeter(twosided, wg) + @test tc isa ToddCoxeter +end + +@testset "TC settings round-trip (8 pairs)" begin + p = Presentation() + set_alphabet!(p, 2) + add_rule_no_checks!(p, _tc_cword(0, 0, 0), _tc_cword(0)) + add_rule_no_checks!(p, _tc_cword(0), _tc_cword(1, 1)) + tc = ToddCoxeter(twosided, p) + + Semigroups.strategy!(tc, strategy_felsch) + @test Semigroups.strategy(tc) == strategy_felsch + + Semigroups.lookahead_extent!(tc, lookahead_extent_full) + @test Semigroups.lookahead_extent(tc) == lookahead_extent_full + + Semigroups.lookahead_style!(tc, lookahead_style_felsch) + @test Semigroups.lookahead_style(tc) == lookahead_style_felsch + + Semigroups.save!(tc, true) + @test Semigroups.save(tc) == true + + Semigroups.use_relations_in_extra!(tc, false) + @test Semigroups.use_relations_in_extra(tc) == false + + Semigroups.lower_bound!(tc, 5) + @test Semigroups.lower_bound(tc) == 5 + + Semigroups.def_version!(tc, def_version_two) + @test Semigroups.def_version(tc) == def_version_two + + Semigroups.def_policy!(tc, def_policy_purge_all) + @test Semigroups.def_policy(tc) == def_policy_purge_all +end + +@testset "TC - current_word_graph after run!" begin + # TC1 (5 classes). Presentation does NOT contain the empty word, so + # number_of_nodes(current_word_graph(tc)) == number_of_classes(tc) + 1 + # (the +1 accounts for the inactive "absorbing" node). + p = Presentation() + set_alphabet!(p, 2) + add_rule_no_checks!(p, _tc_cword(0, 0, 0), _tc_cword(0)) + add_rule_no_checks!(p, _tc_cword(0), _tc_cword(1, 1)) + + tc = ToddCoxeter(twosided, p) + run!(tc) + wg = Semigroups.current_word_graph(tc) + @test wg isa WordGraph + @test number_of_nodes(wg) == 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 Semigroups.is_non_trivial(tc) == tril_TRUE +end + +@testset "TC - tc_redundant_rule" begin + # Irredundant: TC103-style presentation (single rule on a 1-letter alphabet + # cannot be redundant). + p = Presentation() + set_alphabet!(p, 2) + add_rule_no_checks!(p, _tc_cword(0, 0, 0), _tc_cword(0)) + add_rule_no_checks!(p, _tc_cword(0), _tc_cword(1, 1)) + @test Semigroups.tc_redundant_rule(p, Millisecond(50)) === nothing + + # Trivially redundant: add a duplicate rule. + add_rule_no_checks!(p, _tc_cword(0, 0, 0), _tc_cword(0)) + idx = Semigroups.tc_redundant_rule(p, Millisecond(100)) + @test idx isa Integer + @test 1 <= idx <= number_of_rules(p) +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_cword(0, 0, 0), _tc_cword(0)) + add_rule_no_checks!(p, _tc_cword(0), _tc_cword(1, 1)) + tc = ToddCoxeter(twosided, p) + + # contains triggers a run; should agree with index_of equality + @test contains(tc, _tc_cword(0, 0, 1), _tc_cword(0, 0, 0, 0, 1)) + @test currently_contains(tc, _tc_cword(0, 0, 1), _tc_cword(0, 0, 0, 0, 1)) == + tril_TRUE + + # reduce returns the standardized representative + r = Semigroups.reduce(tc, _tc_cword(0, 0, 0, 0, 1)) + @test r isa Vector{Int} + @test contains(tc, r, _tc_cword(0, 0, 0, 0, 1)) + + # normal_forms count == number_of_classes + nfs = normal_forms(tc) + @test length(nfs) == 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_cword(0, 0, 0), _tc_cword(0)) + add_rule_no_checks!(p, _tc_cword(0), _tc_cword(1, 1)) + + tc1 = ToddCoxeter(twosided, p) + @test number_of_classes(tc1) == 5 + + # tc2 is a quotient of tc1 collapsing 0 ~ 1; should produce a smaller + # number_of_classes. + tc2 = ToddCoxeter(twosided, p) + add_generating_pair!(tc2, _tc_cword(0), _tc_cword(1)) + @test number_of_classes(tc2) < number_of_classes(tc1) + + classes = non_trivial_classes(tc1, tc2) + @test classes isa AbstractVector +end + +# ============================================================================ +# Layer 3 — high-level integration +# ============================================================================ + +@testset "ToddCoxeter high-level Julia API" begin + p = Presentation() + set_alphabet!(p, 2) + add_rule_no_checks!(p, _tc_cword(0, 0, 0), _tc_cword(0)) + add_rule_no_checks!(p, _tc_cword(0), _tc_cword(1, 1)) + + tc = ToddCoxeter(twosided, p) + @test length(tc) == number_of_classes(tc) + @test !isempty(sprint(show, tc)) + + # copy: independent objects. Mutate one setting on the copy. + tc2 = copy(tc) + @test length(tc2) == length(tc) + Semigroups.strategy!(tc2, strategy_felsch) + @test Semigroups.strategy(tc2) == strategy_felsch + + # 1-based round-trip for index_of / word_of + standardize!(tc, ORDER_SHORTLEX) + for i in 1:Int(number_of_classes(tc)) + @test Semigroups.index_of(tc, Semigroups.word_of(tc, i)) == i + end + + # Setter chaining (chained left-to-right; both effects must persist). + Semigroups.save!(Semigroups.strategy!(tc, strategy_felsch), false) + @test Semigroups.strategy(tc) == strategy_felsch + @test Semigroups.save(tc) == false + + # standardize! returns a Bool + fresh = ToddCoxeter(twosided, p) + run!(fresh) + @test standardize!(fresh, ORDER_SHORTLEX) isa Bool +end + +# ============================================================================ +# TODO — deferred test cases (re-port when their dependencies land) +# ============================================================================ +# - class_by_index / class_of (deferred; needs Paths-with-alphabet-transform) +# - to<>(tc) conversions (Phase 4b: to(tc), to(tc), +# to(...), to(tc)) +# - Numeric setters (def_max, f_defs, hlt_defs, large_collapse, lookahead_* +# numerics, lookbehind_threshold) — v1.1 +# - perform_lookahead / perform_lookahead_for / perform_lookahead_until / +# perform_lookbehind member callbacks — v1.1 +# - All [extreme]-tagged libsemigroups tests +# - Tests that depend on `presentation::examples::*` not yet exercised +# - RewriteFromLeft-related KB cross-comparisons +# - shrink_to_fit (not yet bound) From fd3e7e50fa5010ccd07a9faeb090f956edc8e70f Mon Sep 17 00:00:00 2001 From: James Swent Date: Sun, 26 Apr 2026 20:11:38 +0100 Subject: [PATCH 05/17] chore: improve coverage on test_todd_coxeter.jl --- test/test_todd_coxeter.jl | 115 +++++++++++++++++++++----------------- 1 file changed, 63 insertions(+), 52 deletions(-) diff --git a/test/test_todd_coxeter.jl b/test/test_todd_coxeter.jl index c9130e2..47850e3 100644 --- a/test/test_todd_coxeter.jl +++ b/test/test_todd_coxeter.jl @@ -49,8 +49,11 @@ const def_version_two = Semigroups.def_version_two # ---- Word conversion helpers (1-based Julia <-> 0-based C++ in libsemigroups) ---- -# Mirror the KB pattern: `_tc_cword(0, 0, 1)` builds the Julia word `[1, 1, 2]`. -_tc_cword(xs::Integer...) = [Int(x) + 1 for x in xs] +# Mirror the KB pattern: `_test_todd_coxeter_cword(0, 0, 1)` builds the Julia +# word `[1, 1, 2]`. Use a long prefix matching the file name so other test +# files included into the same module by `runtests.jl` cannot accidentally +# shadow this helper. +_test_todd_coxeter_cword(xs::Integer...) = [Int(x) + 1 for x in xs] # ============================================================================ # Layer 1 — binding-surface tests @@ -73,11 +76,11 @@ _tc_cword(xs::Integer...) = [Int(x) + 1 for x in xs] # Settings (8 getter / 8 setter pairs) @test hasmethod(Semigroups.strategy, Tuple{ToddCoxeter}) - @test hasmethod(Semigroups.strategy!, Tuple{ToddCoxeter, Any}) + @test hasmethod(Semigroups.strategy!, Tuple{ToddCoxeter, typeof(strategy_hlt)}) @test hasmethod(Semigroups.lookahead_extent, Tuple{ToddCoxeter}) - @test hasmethod(Semigroups.lookahead_extent!, Tuple{ToddCoxeter, Any}) + @test hasmethod(Semigroups.lookahead_extent!, Tuple{ToddCoxeter, typeof(lookahead_extent_full)}) @test hasmethod(Semigroups.lookahead_style, Tuple{ToddCoxeter}) - @test hasmethod(Semigroups.lookahead_style!, Tuple{ToddCoxeter, Any}) + @test hasmethod(Semigroups.lookahead_style!, Tuple{ToddCoxeter, typeof(lookahead_style_hlt)}) @test hasmethod(Semigroups.save, Tuple{ToddCoxeter}) @test hasmethod(Semigroups.save!, Tuple{ToddCoxeter, Bool}) @test hasmethod(Semigroups.use_relations_in_extra, Tuple{ToddCoxeter}) @@ -85,9 +88,9 @@ _tc_cword(xs::Integer...) = [Int(x) + 1 for x in xs] @test hasmethod(Semigroups.lower_bound, Tuple{ToddCoxeter}) @test hasmethod(Semigroups.lower_bound!, Tuple{ToddCoxeter, Integer}) @test hasmethod(Semigroups.def_version, Tuple{ToddCoxeter}) - @test hasmethod(Semigroups.def_version!, Tuple{ToddCoxeter, Any}) + @test hasmethod(Semigroups.def_version!, Tuple{ToddCoxeter, typeof(def_version_one)}) @test hasmethod(Semigroups.def_policy, Tuple{ToddCoxeter}) - @test hasmethod(Semigroups.def_policy!, Tuple{ToddCoxeter, Any}) + @test hasmethod(Semigroups.def_policy!, Tuple{ToddCoxeter, typeof(def_policy_purge_all)}) # Standardize and word-graph access @test hasmethod(standardize!, Tuple{ToddCoxeter, Order}) @@ -137,9 +140,9 @@ end # 2-generator semigroup, rules: 000 = 0, 1111 = 1, 0101 = 00. p = Presentation() set_alphabet!(p, 2) - add_rule_no_checks!(p, _tc_cword(0, 0, 0), _tc_cword(0)) - add_rule_no_checks!(p, _tc_cword(1, 1, 1, 1), _tc_cword(1)) - add_rule_no_checks!(p, _tc_cword(0, 1, 0, 1), _tc_cword(0, 0)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(1, 1, 1, 1), _test_todd_coxeter_cword(1)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 1, 0, 1), _test_todd_coxeter_cword(0, 0)) tc = ToddCoxeter(twosided, p) @test number_of_classes(tc) == 27 @@ -156,8 +159,8 @@ end # 2-generator semigroup, rules: 000 = 0, 0 = 11. p = Presentation() set_alphabet!(p, 2) - add_rule_no_checks!(p, _tc_cword(0, 0, 0), _tc_cword(0)) - add_rule_no_checks!(p, _tc_cword(0), _tc_cword(1, 1)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1, 1)) tc = ToddCoxeter(twosided, p) run!(tc) @@ -165,18 +168,18 @@ end @test finished(tc) # Index-of: 001 == 00001 (1-based: [1,1,2] == [1,1,1,1,2]) - @test Semigroups.index_of(tc, _tc_cword(0, 0, 1)) == - Semigroups.index_of(tc, _tc_cword(0, 0, 0, 0, 1)) - @test Semigroups.index_of(tc, _tc_cword(0, 1, 1, 0, 0, 1)) == - Semigroups.index_of(tc, _tc_cword(0, 0, 0, 0, 1)) - @test Semigroups.index_of(tc, _tc_cword(0, 0, 0)) != - Semigroups.index_of(tc, _tc_cword(1)) + @test Semigroups.index_of(tc, _test_todd_coxeter_cword(0, 0, 1)) == + Semigroups.index_of(tc, _test_todd_coxeter_cword(0, 0, 0, 0, 1)) + @test Semigroups.index_of(tc, _test_todd_coxeter_cword(0, 1, 1, 0, 0, 1)) == + Semigroups.index_of(tc, _test_todd_coxeter_cword(0, 0, 0, 0, 1)) + @test Semigroups.index_of(tc, _test_todd_coxeter_cword(0, 0, 0)) != + Semigroups.index_of(tc, _test_todd_coxeter_cword(1)) # Standardize for shortlex (TC001 lines 371-374) standardize!(tc, ORDER_SHORTLEX) - @test Semigroups.word_of(tc, 1) == _tc_cword(0) # C++ index 0 - @test Semigroups.word_of(tc, 2) == _tc_cword(1) # C++ index 1 - @test Semigroups.word_of(tc, 3) == _tc_cword(0, 0) # C++ index 2 + @test Semigroups.word_of(tc, 1) == _test_todd_coxeter_cword(0) # C++ index 0 + @test Semigroups.word_of(tc, 2) == _test_todd_coxeter_cword(1) # C++ index 1 + @test Semigroups.word_of(tc, 3) == _test_todd_coxeter_cword(0, 0) # C++ index 2 # Standardize for lex (TC001 lines 375-391) standardize!(tc, ORDER_LEX) @@ -184,11 +187,11 @@ end @test Semigroups.is_standardized(tc) @test !Semigroups.is_standardized(tc, ORDER_SHORTLEX) - @test Semigroups.word_of(tc, 1) == _tc_cword(0) # 0 - @test Semigroups.word_of(tc, 2) == _tc_cword(0, 0) # 00 - @test Semigroups.word_of(tc, 3) == _tc_cword(0, 0, 1) # 001 - @test Semigroups.word_of(tc, 4) == _tc_cword(0, 0, 1, 0) # 0010 - @test Semigroups.word_of(tc, 5) == _tc_cword(1) # 1 + @test Semigroups.word_of(tc, 1) == _test_todd_coxeter_cword(0) # 0 + @test Semigroups.word_of(tc, 2) == _test_todd_coxeter_cword(0, 0) # 00 + @test Semigroups.word_of(tc, 3) == _test_todd_coxeter_cword(0, 0, 1) # 001 + @test Semigroups.word_of(tc, 4) == _test_todd_coxeter_cword(0, 0, 1, 0) # 0010 + @test Semigroups.word_of(tc, 5) == _test_todd_coxeter_cword(1) # 1 # word_of/index_of round-trip (1-based on the Julia side) for i in 1:5 @@ -199,11 +202,11 @@ end standardize!(tc, ORDER_SHORTLEX) @test Semigroups.is_standardized(tc, ORDER_SHORTLEX) @test normal_forms(tc) == [ - _tc_cword(0), - _tc_cword(1), - _tc_cword(0, 0), - _tc_cword(0, 1), - _tc_cword(0, 0, 1), + _test_todd_coxeter_cword(0), + _test_todd_coxeter_cword(1), + _test_todd_coxeter_cword(0, 0), + _test_todd_coxeter_cword(0, 1), + _test_todd_coxeter_cword(0, 0, 1), ] end @@ -211,31 +214,39 @@ end # Port of libsemigroups TC025 (test-todd-coxeter.cpp:1447-1468), reduced. p = Presentation() set_alphabet!(p, 2) - add_rule_no_checks!(p, _tc_cword(0, 0, 0), _tc_cword(0)) - add_rule_no_checks!(p, _tc_cword(0), _tc_cword(1, 1)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1, 1)) tc1 = ToddCoxeter(twosided, p) @test number_of_classes(tc1) == 5 tc2 = ToddCoxeter(onesided, tc1) - add_generating_pair!(tc2, _tc_cword(0), _tc_cword(0, 0)) + add_generating_pair!(tc2, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(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, so mirror that and + # add a couple of cheap positive observations about the resulting object's + # state. The 1-node, 2-out-degree WordGraph has all-UNDEFINED targets, so + # the only thing safe to assert about `number_of_classes` is that it is a + # non-negative integer. 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 + # Runner-state queries should not throw on a freshly constructed object. + @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_cword(0, 0, 0), _tc_cword(0)) - add_rule_no_checks!(p, _tc_cword(0), _tc_cword(1, 1)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1, 1)) tc = ToddCoxeter(twosided, p) Semigroups.strategy!(tc, strategy_felsch) @@ -269,8 +280,8 @@ end # (the +1 accounts for the inactive "absorbing" node). p = Presentation() set_alphabet!(p, 2) - add_rule_no_checks!(p, _tc_cword(0, 0, 0), _tc_cword(0)) - add_rule_no_checks!(p, _tc_cword(0), _tc_cword(1, 1)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1, 1)) tc = ToddCoxeter(twosided, p) run!(tc) @@ -293,12 +304,12 @@ end # cannot be redundant). p = Presentation() set_alphabet!(p, 2) - add_rule_no_checks!(p, _tc_cword(0, 0, 0), _tc_cword(0)) - add_rule_no_checks!(p, _tc_cword(0), _tc_cword(1, 1)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1, 1)) @test Semigroups.tc_redundant_rule(p, Millisecond(50)) === nothing # Trivially redundant: add a duplicate rule. - add_rule_no_checks!(p, _tc_cword(0, 0, 0), _tc_cword(0)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) idx = Semigroups.tc_redundant_rule(p, Millisecond(100)) @test idx isa Integer @test 1 <= idx <= number_of_rules(p) @@ -307,19 +318,19 @@ 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_cword(0, 0, 0), _tc_cword(0)) - add_rule_no_checks!(p, _tc_cword(0), _tc_cword(1, 1)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1, 1)) tc = ToddCoxeter(twosided, p) # contains triggers a run; should agree with index_of equality - @test contains(tc, _tc_cword(0, 0, 1), _tc_cword(0, 0, 0, 0, 1)) - @test currently_contains(tc, _tc_cword(0, 0, 1), _tc_cword(0, 0, 0, 0, 1)) == + @test contains(tc, _test_todd_coxeter_cword(0, 0, 1), _test_todd_coxeter_cword(0, 0, 0, 0, 1)) + @test currently_contains(tc, _test_todd_coxeter_cword(0, 0, 1), _test_todd_coxeter_cword(0, 0, 0, 0, 1)) == tril_TRUE # reduce returns the standardized representative - r = Semigroups.reduce(tc, _tc_cword(0, 0, 0, 0, 1)) + r = Semigroups.reduce(tc, _test_todd_coxeter_cword(0, 0, 0, 0, 1)) @test r isa Vector{Int} - @test contains(tc, r, _tc_cword(0, 0, 0, 0, 1)) + @test contains(tc, r, _test_todd_coxeter_cword(0, 0, 0, 0, 1)) # normal_forms count == number_of_classes nfs = normal_forms(tc) @@ -329,8 +340,8 @@ 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_cword(0, 0, 0), _tc_cword(0)) - add_rule_no_checks!(p, _tc_cword(0), _tc_cword(1, 1)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1, 1)) tc1 = ToddCoxeter(twosided, p) @test number_of_classes(tc1) == 5 @@ -338,7 +349,7 @@ end # tc2 is a quotient of tc1 collapsing 0 ~ 1; should produce a smaller # number_of_classes. tc2 = ToddCoxeter(twosided, p) - add_generating_pair!(tc2, _tc_cword(0), _tc_cword(1)) + add_generating_pair!(tc2, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1)) @test number_of_classes(tc2) < number_of_classes(tc1) classes = non_trivial_classes(tc1, tc2) @@ -352,8 +363,8 @@ end @testset "ToddCoxeter high-level Julia API" begin p = Presentation() set_alphabet!(p, 2) - add_rule_no_checks!(p, _tc_cword(0, 0, 0), _tc_cword(0)) - add_rule_no_checks!(p, _tc_cword(0), _tc_cword(1, 1)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) + add_rule_no_checks!(p, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1, 1)) tc = ToddCoxeter(twosided, p) @test length(tc) == number_of_classes(tc) From 95979443cd665ccea824f01b8a605a099d275bf4 Mon Sep 17 00:00:00 2001 From: James Swent Date: Sun, 26 Apr 2026 21:21:08 +0100 Subject: [PATCH 06/17] feat: add ToddCoxeter init! overloads --- src/todd-coxeter.jl | 51 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/todd-coxeter.jl b/src/todd-coxeter.jl index 28223ca..1c0b66c 100644 --- a/src/todd-coxeter.jl +++ b/src/todd-coxeter.jl @@ -211,3 +211,54 @@ const def_version_two = LibSemigroups.def_version_two @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`. + +The one-argument form clears the underlying word graph, presentation, +generating pairs, settings, and statistics from `tc`, putting it back into +the same state as a newly default-constructed [`ToddCoxeter`](@ref +Semigroups.ToddCoxeter). + +The three-argument forms reinitialize `tc` as if it had just been +constructed from the corresponding arguments — `(kind, p)` for a +[`Presentation`](@ref Semigroups.Presentation), `(kind, other)` for a +quotient construction from another `ToddCoxeter`, or `(kind, wg)` for a +construction from 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). +""" +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 From 94137102736047bc3558cd40db0871762bd9224d Mon Sep 17 00:00:00 2001 From: James Swent Date: Sun, 26 Apr 2026 21:25:03 +0100 Subject: [PATCH 07/17] feat: add ToddCoxeter settings --- src/todd-coxeter.jl | 249 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) diff --git a/src/todd-coxeter.jl b/src/todd-coxeter.jl index 1c0b66c..5991184 100644 --- a/src/todd-coxeter.jl +++ b/src/todd-coxeter.jl @@ -262,3 +262,252 @@ 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), +[`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 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 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 deductions made during HLT enumeration are processed in +the same way as those made during Felsch enumeration. + +# See also + +[`save!`](@ref Semigroups.save!) +""" +save(tc::ToddCoxeter) = LibSemigroups.save(tc) + +""" + save!(tc::ToddCoxeter, val::Bool) -> ToddCoxeter + +Set the value of the `save` setting on `tc`. Returns `tc` for chaining. + +If `val` is `true`, deductions made during HLT enumeration are processed +in the same way as those made during Felsch enumeration. This typically +slows down HLT enumeration but may 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 the relations of the underlying presentation are used in +the Felsch part of the algorithm when applied to the generating pairs. + +# 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 the relations of the underlying presentation are used in +the Felsch part of the algorithm when applied to the generating pairs. +Returns `tc` for chaining. + +# 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) -> Int + +Return the current lower bound on the number of classes of the congruence +represented by `tc`. A value of `0` means no bound has been set. + +# See also + +[`lower_bound!`](@ref Semigroups.lower_bound!) +""" +lower_bound(tc::ToddCoxeter) = Int(LibSemigroups.lower_bound(tc)) + +""" + lower_bound!(tc::ToddCoxeter, val::Integer) -> ToddCoxeter + +Set a lower bound on the number of classes of the congruence represented +by `tc` to `val`. Returns `tc` for chaining. + +If the number of currently active nodes during enumeration reaches `val` +and the word graph is complete, the algorithm can stop early. A value of +`0` indicates no lower bound. + +# 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 + +""" + def_version(tc::ToddCoxeter) + +Return the current definition-routine version of `tc`. + +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 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 From 73e5beadeab4266c3971539dd577ee60efd113c4 Mon Sep 17 00:00:00 2001 From: James Swent Date: Sun, 26 Apr 2026 21:28:50 +0100 Subject: [PATCH 08/17] feat: add ToddCoxeter standardize and word_graph accessors --- src/todd-coxeter.jl | 79 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/todd-coxeter.jl b/src/todd-coxeter.jl index 5991184..4e0c2aa 100644 --- a/src/todd-coxeter.jl +++ b/src/todd-coxeter.jl @@ -511,3 +511,82 @@ 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 underlying word graph of `tc` according to the order +`ord`. + +Standardization renumbers the nodes of the word graph so that the words +labelling its nodes appear in the order specified by `ord`. Returns +`true` if the underlying word graph was modified, `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 underlying word graph of `tc` is standardized. + +The one-argument form returns `true` if the word graph has been +standardized according to *some* order. The two-argument form returns +`true` only if it has been standardized according to `ord`. + +# 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 current state of the underlying word graph of `tc`, without +triggering a run. + +If `tc` has not been run, the returned word graph reflects whatever +incomplete state has been built so far. + +# 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 running the algorithm to +completion and standardizing. + +This function triggers a full enumeration of `tc`, which may never +terminate. + +# See also + +[`current_word_graph`](@ref Semigroups.current_word_graph) +""" +@cxxdereference word_graph(tc::ToddCoxeter) = LibSemigroups.word_graph(tc) From 8d0c7fd1a04c17fee1ec00de411180e4b6387499 Mon Sep 17 00:00:00 2001 From: James Swent Date: Sun, 26 Apr 2026 21:38:04 +0100 Subject: [PATCH 09/17] feat: add ToddCoxeter index<->word conversions --- src/todd-coxeter.jl | 121 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/src/todd-coxeter.jl b/src/todd-coxeter.jl index 4e0c2aa..0db7b12 100644 --- a/src/todd-coxeter.jl +++ b/src/todd-coxeter.jl @@ -590,3 +590,124 @@ terminate. [`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`. + +This function triggers a full enumeration of `tc`, which may never +terminate. + +# 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 + +[`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). + +# 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 a run. + +# 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 From f2b9b35c498d7d27ab93a79a8a1140947c9f0d1a Mon Sep 17 00:00:00 2001 From: James Swent Date: Sun, 26 Apr 2026 21:40:54 +0100 Subject: [PATCH 10/17] feat: add ToddCoxeter query methods and Base overloads --- src/todd-coxeter.jl | 95 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/todd-coxeter.jl b/src/todd-coxeter.jl index 0db7b12..f3158d2 100644 --- a/src/todd-coxeter.jl +++ b/src/todd-coxeter.jl @@ -711,3 +711,98 @@ function current_word_of(tc::ToddCoxeter, i::Integer) out = @wrap_libsemigroups_call LibSemigroups.current_word_of(tc, cpp_i) return _word_from_cpp(out) 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. + +!!! 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 of the congruence represented by `tc` — either +[`twosided`](@ref Semigroups.twosided) or +[`onesided`](@ref Semigroups.onesided). +""" +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 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. +""" +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 by `tc`. +""" +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) From 930727e8cbac2f25a52486dfccc6caf3dc0fd0ca Mon Sep 17 00:00:00 2001 From: James Swent Date: Sun, 26 Apr 2026 21:42:44 +0100 Subject: [PATCH 11/17] feat: add ToddCoxeter is_non_trivial helper --- src/todd-coxeter.jl | 52 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/todd-coxeter.jl b/src/todd-coxeter.jl index f3158d2..758226b 100644 --- a/src/todd-coxeter.jl +++ b/src/todd-coxeter.jl @@ -806,3 +806,55 @@ 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, +) + ns = convert(Nanosecond, try_for) + return @wrap_libsemigroups_call LibSemigroups.tc_is_non_trivial( + tc, + UInt(tries), + Int64(Dates.value(ns)), + Float32(threshold), + ) +end From d92de1abf470ed16e834536f036e8195e13a1904 Mon Sep 17 00:00:00 2001 From: James Swent Date: Sun, 26 Apr 2026 21:44:18 +0100 Subject: [PATCH 12/17] feat: add ToddCoxeter tc_redundant_rule helper --- src/todd-coxeter.jl | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/todd-coxeter.jl b/src/todd-coxeter.jl index 758226b..8755885 100644 --- a/src/todd-coxeter.jl +++ b/src/todd-coxeter.jl @@ -858,3 +858,44 @@ function is_non_trivial( Float32(threshold), ) 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) + ns = convert(Nanosecond, timeout) + idx = @wrap_libsemigroups_call LibSemigroups.tc_redundant_rule( + p, + Int64(Dates.value(ns)), + ) + n_flat = 2 * number_of_rules(p) + if idx >= n_flat + return nothing + end + return div(Int(idx), 2) + 1 +end From 94bed0f758a98e4204b2e7746dbf0ac030350e56 Mon Sep 17 00:00:00 2001 From: James Swent Date: Sun, 26 Apr 2026 22:43:36 +0100 Subject: [PATCH 13/17] feat: add ToddCoxeter exports --- src/Semigroups.jl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Semigroups.jl b/src/Semigroups.jl index 11be162..2d0149d 100644 --- a/src/Semigroups.jl +++ b/src/Semigroups.jl @@ -354,6 +354,26 @@ 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 From 235fbc8cba2c5328a8195b7ff5b5a669c19e7f28 Mon Sep 17 00:00:00 2001 From: James Swent Date: Sun, 26 Apr 2026 22:49:12 +0100 Subject: [PATCH 14/17] fix: correct test_todd_coxeter.jl assertions for current_word_graph and contains --- test/test_todd_coxeter.jl | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/test/test_todd_coxeter.jl b/test/test_todd_coxeter.jl index 47850e3..db80d5f 100644 --- a/test/test_todd_coxeter.jl +++ b/test/test_todd_coxeter.jl @@ -124,7 +124,7 @@ _test_todd_coxeter_cword(xs::Integer...) = [Int(x) + 1 for x in xs] # Inherited from CongruenceCommon (already wrapped in src/cong-common.jl) @test hasmethod(add_generating_pair!, Tuple{Semigroups.CongruenceCommon, AbstractVector{<:Integer}, AbstractVector{<:Integer}}) @test hasmethod(currently_contains, Tuple{Semigroups.CongruenceCommon, AbstractVector{<:Integer}, AbstractVector{<:Integer}}) - @test hasmethod(contains, Tuple{Semigroups.CongruenceCommon, AbstractVector{<:Integer}, AbstractVector{<:Integer}}) + @test hasmethod(Semigroups.contains, Tuple{Semigroups.CongruenceCommon, AbstractVector{<:Integer}, AbstractVector{<:Integer}}) @test hasmethod(Semigroups.reduce, Tuple{Semigroups.CongruenceCommon, AbstractVector{<:Integer}}) @test hasmethod(reduce_no_run, Tuple{Semigroups.CongruenceCommon, AbstractVector{<:Integer}}) @test hasmethod(normal_forms, Tuple{Semigroups.CongruenceCommon}) @@ -275,9 +275,11 @@ end end @testset "TC - current_word_graph after run!" begin - # TC1 (5 classes). Presentation does NOT contain the empty word, so - # number_of_nodes(current_word_graph(tc)) == number_of_classes(tc) + 1 - # (the +1 accounts for the inactive "absorbing" node). + # TC1 (5 classes). 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 that is not a real class + # representative when the presentation does NOT contain the empty word). + # word_graph(tc) standardizes and prunes, giving exactly that count. p = Presentation() set_alphabet!(p, 2) add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) @@ -285,9 +287,15 @@ end tc = ToddCoxeter(twosided, p) run!(tc) - wg = Semigroups.current_word_graph(tc) - @test wg isa WordGraph - @test number_of_nodes(wg) == number_of_classes(tc) + 1 + cwg = Semigroups.current_word_graph(tc) + # current_word_graph returns a CxxBaseRef{WordGraph}; check usability via + # number_of_nodes (which accepts either WordGraph or its CxxBaseRef). + @test number_of_nodes(cwg) >= number_of_classes(tc) + 1 + + # word_graph(tc) returns the standardized, pruned graph; for a + # presentation without the empty word, this is exactly classes + 1. + swg = Semigroups.word_graph(tc) + @test number_of_nodes(swg) == number_of_classes(tc) + 1 end @testset "TC - is_non_trivial on free monogenic" begin @@ -323,14 +331,14 @@ end tc = ToddCoxeter(twosided, p) # contains triggers a run; should agree with index_of equality - @test contains(tc, _test_todd_coxeter_cword(0, 0, 1), _test_todd_coxeter_cword(0, 0, 0, 0, 1)) + @test Semigroups.contains(tc, _test_todd_coxeter_cword(0, 0, 1), _test_todd_coxeter_cword(0, 0, 0, 0, 1)) @test currently_contains(tc, _test_todd_coxeter_cword(0, 0, 1), _test_todd_coxeter_cword(0, 0, 0, 0, 1)) == tril_TRUE # reduce returns the standardized representative r = Semigroups.reduce(tc, _test_todd_coxeter_cword(0, 0, 0, 0, 1)) @test r isa Vector{Int} - @test contains(tc, r, _test_todd_coxeter_cword(0, 0, 0, 0, 1)) + @test Semigroups.contains(tc, r, _test_todd_coxeter_cword(0, 0, 0, 0, 1)) # normal_forms count == number_of_classes nfs = normal_forms(tc) From 0e44c80dc0b1f299b12d5dfebc20a7c5af5e5919 Mon Sep 17 00:00:00 2001 From: James Swent Date: Sun, 26 Apr 2026 22:49:29 +0100 Subject: [PATCH 15/17] chore: polish ToddCoxeter docstrings and add tc_redundant_rule comment --- src/todd-coxeter.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/todd-coxeter.jl b/src/todd-coxeter.jl index 8755885..311585e 100644 --- a/src/todd-coxeter.jl +++ b/src/todd-coxeter.jl @@ -815,7 +815,7 @@ Base.deepcopy_internal(tc::ToddCoxeter, ::IdDict) = is_non_trivial(tc::ToddCoxeter; tries::Integer = 10, try_for::TimePeriod = Dates.Millisecond(100), - threshold::Real = 0.99) -> tril + threshold::Real = 0.99) -> [`tril`](@ref Semigroups.tril) Heuristically check whether the congruence represented by `tc` is non-trivial. @@ -897,5 +897,7 @@ function tc_redundant_rule(p::Presentation, timeout::TimePeriod) 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 From 6ce83edff107044f72e67a68d2fd3e87707f3120 Mon Sep 17 00:00:00 2001 From: James Swent Date: Mon, 27 Apr 2026 11:11:07 +0100 Subject: [PATCH 16/17] chore: fixes, format, and improve docs --- deps/src/cong-common.cpp | 23 +- deps/src/cong-common.hpp | 55 +++-- deps/src/knuth-bendix.cpp | 7 + deps/src/libsemigroups_julia.cpp | 2 - deps/src/libsemigroups_julia.hpp | 4 +- deps/src/todd-coxeter.cpp | 106 +++------ src/Semigroups.jl | 3 +- src/todd-coxeter.jl | 374 ++++++++++++++++++----------- test/test_todd_coxeter.jl | 394 ++++++++++++++----------------- 9 files changed, 490 insertions(+), 478 deletions(-) diff --git a/deps/src/cong-common.cpp b/deps/src/cong-common.cpp index 28e7341..1c6129f 100644 --- a/deps/src/cong-common.cpp +++ b/deps/src/cong-common.cpp @@ -18,16 +18,9 @@ // CRITICAL: libsemigroups_julia.hpp MUST be included first (fmt consteval fix) #include "libsemigroups_julia.hpp" -// libsemigroups headers that provide algorithm-specific overloads of -// congruence_common helpers MUST be included BEFORE cong-common.hpp so the -// template bodies in cong-common.hpp pick them up at instantiation. #include -#include -#include #include -#include "cong-common.hpp" - #include namespace jlcxx { @@ -48,20 +41,12 @@ namespace libsemigroups_julia { 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 index 0bdf9b5..d298f6b 100644 --- a/deps/src/cong-common.hpp +++ b/deps/src/cong-common.hpp @@ -78,8 +78,7 @@ namespace libsemigroups_julia { jlcxx::ArrayRef v) -> bool { Word uw(u.begin(), u.end()); Word vw(v.begin(), v.end()); - return libsemigroups::congruence_common::contains( - self, uw, vw); + return libsemigroups::congruence_common::contains(self, uw, vw); }); // currently_contains (no enumeration, returns tril) @@ -94,15 +93,13 @@ namespace libsemigroups_julia { }); // 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_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) @@ -125,16 +122,15 @@ namespace libsemigroups_julia { // 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; - }); + 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 @@ -143,11 +139,24 @@ namespace libsemigroups_julia { m.method("cong_common_non_trivial_classes", [](Thing& x, Thing& y) -> std::vector> { - return libsemigroups::congruence_common::non_trivial_classes( - x, y); + 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 ab72407..94b2d52 100644 --- a/deps/src/libsemigroups_julia.cpp +++ b/deps/src/libsemigroups_julia.cpp @@ -55,8 +55,6 @@ namespace libsemigroups_julia { define_presentation_examples(mod); define_knuth_bendix(mod); define_todd_coxeter(mod); - define_knuth_bendix_cong_common_helpers(mod); - define_todd_coxeter_cong_common_helpers(mod); } } // namespace libsemigroups_julia diff --git a/deps/src/libsemigroups_julia.hpp b/deps/src/libsemigroups_julia.hpp index 8980da3..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,9 +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); - void define_todd_coxeter_cong_common_helpers(jl::Module& mod); } // namespace libsemigroups_julia diff --git a/deps/src/todd-coxeter.cpp b/deps/src/todd-coxeter.cpp index 0685379..cdb9e8a 100644 --- a/deps/src/todd-coxeter.cpp +++ b/deps/src/todd-coxeter.cpp @@ -72,10 +72,6 @@ namespace libsemigroups_julia { using TCImpl = libsemigroups::detail::ToddCoxeterImpl; using TC = libsemigroups::ToddCoxeter; - //////////////////////////////////////////////////////////////////////// - // Enums - //////////////////////////////////////////////////////////////////////// - // 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", @@ -98,8 +94,7 @@ namespace libsemigroups_julia { // 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_hlt", TCImpl::options::lookahead_style::hlt); m.set_const("lookahead_style_felsch", TCImpl::options::lookahead_style::felsch); @@ -110,12 +105,10 @@ namespace libsemigroups_julia { 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_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); + 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) @@ -124,28 +117,19 @@ namespace libsemigroups_julia { 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, @@ -157,23 +141,17 @@ namespace libsemigroups_julia { [](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); }); - - //////////////////////////////////////////////////////////////////////// - // Settings (getter / setter pairs with DISTINCT names) - //////////////////////////////////////////////////////////////////////// + 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); - }); + type.method("set_strategy!", [](TC& self, TCImpl::options::strategy val) { + self.strategy(val); + }); // lookahead_extent type.method("lookahead_extent", @@ -232,10 +210,6 @@ namespace libsemigroups_julia { self.def_policy(val); }); - //////////////////////////////////////////////////////////////////////// - // Standardize / word-graph access - //////////////////////////////////////////////////////////////////////// - type.method("standardize!", [](TC& self, Order ord) -> bool { return self.standardize(ord); }); @@ -254,14 +228,9 @@ namespace libsemigroups_julia { }); // word_graph: triggers run; non-const this. - type.method("word_graph", - [](TC& self) -> WordGraph const& { - return self.word_graph(); - }); - - //////////////////////////////////////////////////////////////////////// - // Word <-> class index - //////////////////////////////////////////////////////////////////////// + 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 { @@ -269,17 +238,21 @@ namespace libsemigroups_julia { 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("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("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 { @@ -288,10 +261,6 @@ namespace libsemigroups_julia { return out; }); - //////////////////////////////////////////////////////////////////////// - // Query methods - //////////////////////////////////////////////////////////////////////// - type.method("number_of_classes", [](TC& self) -> uint64_t { return self.number_of_classes(); }); @@ -313,26 +282,18 @@ namespace libsemigroups_julia { return self.presentation(); }); - //////////////////////////////////////////////////////////////////////// - // Display - //////////////////////////////////////////////////////////////////////// - type.method("to_human_readable_repr", [](TC const& self) -> std::string { return libsemigroups::to_human_readable_repr(self); }); - //////////////////////////////////////////////////////////////////////// - // Free functions (todd_coxeter:: namespace) - //////////////////////////////////////////////////////////////////////// - // 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::milliseconds>( - std::chrono::nanoseconds(try_for_ns)); + 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); }); @@ -345,15 +306,8 @@ namespace libsemigroups_julia { p, std::chrono::nanoseconds(ns)); return static_cast(std::distance(p.rules.cbegin(), it)); }); - } - - void define_todd_coxeter_cong_common_helpers(jl::Module& m) { - using libsemigroups::word_type; - using TC = libsemigroups::ToddCoxeter; - define_cong_common_word_helpers(m); - define_cong_common_normal_forms(m); - define_cong_common_non_trivial_classes(m); + define_cong_common_helpers(m); } } // namespace libsemigroups_julia diff --git a/src/Semigroups.jl b/src/Semigroups.jl index 2d0149d..9d143e7 100644 --- a/src/Semigroups.jl +++ b/src/Semigroups.jl @@ -356,7 +356,8 @@ 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 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 diff --git a/src/todd-coxeter.jl b/src/todd-coxeter.jl index 311585e..c0fc9c7 100644 --- a/src/todd-coxeter.jl +++ b/src/todd-coxeter.jl @@ -42,7 +42,8 @@ objects. - `LibsemigroupsError` if `p` is not valid (Presentation form). - `LibsemigroupsError` if `kind` and `tc.kind()` are incompatible - (ToddCoxeter form). + (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 @@ -55,33 +56,40 @@ const ToddCoxeter = LibSemigroups.ToddCoxeterWord """ strategy_hlt -Strategy enum value: HLT (Hazelgrove-Leech-Trotter) coset enumeration. -See [`strategy!`](@ref Semigroups.strategy!). +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: Felsch-style coset enumeration. Definitions are made -greedily, applying the relations of the presentation immediately to identify -classes. See [`strategy!`](@ref Semigroups.strategy!). +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: alternating between HLT (Cosets) and Felsch (Relations). -See Holt, Eick, O'Brien, *Handbook of Computational Group Theory*, §5.3. -See [`strategy!`](@ref Semigroups.strategy!). +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: HLT phase, followed by Felsch phase. +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 @@ -89,16 +97,22 @@ const strategy_R_over_C = LibSemigroups.strategy_R_over_C """ strategy_Cr -Strategy enum value: short Felsch phase, followed by HLT, with one final -Felsch sweep at the end. See [`strategy!`](@ref Semigroups.strategy!). +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: short HLT phase, followed by Felsch, with one final -HLT sweep at the end. See [`strategy!`](@ref Semigroups.strategy!). +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 @@ -107,16 +121,20 @@ const strategy_Rc = LibSemigroups.strategy_Rc """ lookahead_extent_full -Lookahead-extent enum value: lookahead processes the full word graph. -See [`lookahead_extent!`](@ref Semigroups.lookahead_extent!). +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: lookahead processes only part of the word -graph. See [`lookahead_extent!`](@ref Semigroups.lookahead_extent!). +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 @@ -125,16 +143,19 @@ const lookahead_extent_partial = LibSemigroups.lookahead_extent_partial """ lookahead_style_hlt -Lookahead-style enum value: HLT-style lookahead. -See [`lookahead_style!`](@ref Semigroups.lookahead_style!). +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. -See [`lookahead_style!`](@ref Semigroups.lookahead_style!). +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 @@ -143,42 +164,44 @@ const lookahead_style_felsch = LibSemigroups.lookahead_style_felsch """ def_policy_no_stack_if_no_space -Definition-policy enum value: do not stack a deduction if there is no space -left. See [`def_policy!`](@ref Semigroups.def_policy!). +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 +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: purge from the top of the deduction stack -when full. See [`def_policy!`](@ref Semigroups.def_policy!). +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: purge all deductions when the stack is full. -See [`def_policy!`](@ref Semigroups.def_policy!). +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: discard all deductions if there is no space -left. See [`def_policy!`](@ref Semigroups.def_policy!). +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 +const def_policy_discard_all_if_no_space = LibSemigroups.def_policy_discard_all_if_no_space """ def_policy_unlimited -Definition-policy enum value: do not limit the number of stacked deductions. -See [`def_policy!`](@ref Semigroups.def_policy!). +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 @@ -187,16 +210,18 @@ const def_policy_unlimited = LibSemigroups.def_policy_unlimited """ def_version_one -Definition-version enum value: version 1 of the definition routine. -See [`def_version!`](@ref Semigroups.def_version!). +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: version 2 of the definition routine. -See [`def_version!`](@ref Semigroups.def_version!). +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 @@ -209,8 +234,7 @@ const def_version_two = LibSemigroups.def_version_two # 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 +@inline _index_from_cpp(i::Integer) = i == typemax(UInt) ? UNDEFINED : Int(i) + 1 # ============================================================================ # Initialization @@ -222,18 +246,18 @@ const def_version_two = LibSemigroups.def_version_two init!(tc::ToddCoxeter, kind::congruence_kind, other::ToddCoxeter) -> ToddCoxeter init!(tc::ToddCoxeter, kind::congruence_kind, wg::WordGraph) -> ToddCoxeter -Re-initialize `tc`. +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 from `tc`, putting it back into -the same state as a newly default-constructed [`ToddCoxeter`](@ref -Semigroups.ToddCoxeter). +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` as if it had just been -constructed from the corresponding arguments — `(kind, p)` for a -[`Presentation`](@ref Semigroups.Presentation), `(kind, other)` for a -quotient construction from another `ToddCoxeter`, or `(kind, wg)` for a -construction from a [`WordGraph`](@ref Semigroups.WordGraph). +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. @@ -241,7 +265,8 @@ Returns `tc` for chaining. - `LibsemigroupsError` if `p` is not valid (Presentation form). - `LibsemigroupsError` if `kind` and `other.kind()` are incompatible - (ToddCoxeter form). + (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) @@ -264,7 +289,7 @@ function init!(tc::ToddCoxeter, kind::congruence_kind, wg::WordGraph) end # ============================================================================ -# Settings — getter / setter pairs +# Settings - getter / setter pairs # ============================================================================ """ @@ -272,8 +297,8 @@ end Return the current coset enumeration strategy of `tc`. -The returned value is one of [`strategy_hlt`](@ref Semigroups.strategy_hlt), -[`strategy_felsch`](@ref Semigroups.strategy_felsch), +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 @@ -303,7 +328,8 @@ end """ lookahead_extent(tc::ToddCoxeter) -Return the current lookahead extent of `tc`. +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 @@ -332,7 +358,8 @@ end """ lookahead_style(tc::ToddCoxeter) -Return the current lookahead style of `tc`. +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 @@ -361,8 +388,8 @@ end """ save(tc::ToddCoxeter) -> Bool -Return whether deductions made during HLT enumeration are processed in -the same way as those made during Felsch enumeration. +Return whether definitions are processed during HLT-style enumeration. The +default is `false`. # See also @@ -373,12 +400,12 @@ save(tc::ToddCoxeter) = LibSemigroups.save(tc) """ save!(tc::ToddCoxeter, val::Bool) -> ToddCoxeter -Set the value of the `save` setting on `tc`. Returns `tc` for chaining. +Set whether definitions are processed during any HLT-style enumeration of +`tc`. Returns `tc` for chaining. The default is `false`. -If `val` is `true`, deductions made during HLT enumeration are processed -in the same way as those made during Felsch enumeration. This typically -slows down HLT enumeration but may reduce the size of the underlying word -graph. +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 @@ -392,22 +419,23 @@ end """ use_relations_in_extra(tc::ToddCoxeter) -> Bool -Return whether the relations of the underlying presentation are used in -the Felsch part of the algorithm when applied to the generating pairs. +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) = LibSemigroups.use_relations_in_extra(tc) """ use_relations_in_extra!(tc::ToddCoxeter, val::Bool) -> ToddCoxeter -Set whether the relations of the underlying presentation are used in -the Felsch part of the algorithm when applied to the generating pairs. -Returns `tc` for chaining. +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 @@ -419,26 +447,33 @@ function use_relations_in_extra!(tc::ToddCoxeter, val::Bool) end """ - lower_bound(tc::ToddCoxeter) -> Int + lower_bound(tc::ToddCoxeter) -> Union{Int, UndefinedType} Return the current lower bound on the number of classes of the congruence -represented by `tc`. A value of `0` means no bound has been set. +represented by `tc`. The default is [`UNDEFINED`](@ref Semigroups.UNDEFINED) +(no bound set); otherwise an `Int`. # See also [`lower_bound!`](@ref Semigroups.lower_bound!) """ -lower_bound(tc::ToddCoxeter) = Int(LibSemigroups.lower_bound(tc)) +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` to `val`. Returns `tc` for chaining. +Set a lower bound on the number of classes of the congruence represented by +`tc`. Returns `tc` for chaining. -If the number of currently active nodes during enumeration reaches `val` -and the word graph is complete, the algorithm can stop early. A value of -`0` indicates no lower bound. +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 @@ -449,10 +484,14 @@ function lower_bound!(tc::ToddCoxeter, val::Integer) 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`. +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 @@ -482,7 +521,10 @@ end """ def_policy(tc::ToddCoxeter) -Return the current definition-stack policy of `tc`. +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), @@ -519,17 +561,17 @@ end """ standardize!(tc::ToddCoxeter, ord::Order) -> Bool -Standardize the underlying word graph of `tc` according to the order -`ord`. +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. -Standardization renumbers the nodes of the word graph so that the words -labelling its nodes appear in the order specified by `ord`. Returns -`true` if the underlying word graph was modified, `false` otherwise. +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 +- `ord::Order`: the order to standardize by - typically [`ORDER_SHORTLEX`](@ref Semigroups.ORDER_SHORTLEX) or [`ORDER_LEX`](@ref Semigroups.ORDER_LEX). @@ -545,11 +587,19 @@ end is_standardized(tc::ToddCoxeter) -> Bool is_standardized(tc::ToddCoxeter, ord::Order) -> Bool -Return whether the underlying word graph of `tc` is standardized. +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 according to *some* order. The two-argument form returns -`true` only if it has been standardized according to `ord`. +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 @@ -557,33 +607,44 @@ standardized according to *some* order. The two-argument form returns """ is_standardized(tc::ToddCoxeter) = LibSemigroups.is_standardized(tc) -is_standardized(tc::ToddCoxeter, ord::Order) = - LibSemigroups.is_standardized(tc, ord) +is_standardized(tc::ToddCoxeter, ord::Order) = LibSemigroups.is_standardized(tc, ord) """ current_word_graph(tc::ToddCoxeter) -> WordGraph -Return the current state of the underlying word graph of `tc`, without -triggering a run. +Return the underlying word graph of `tc` in its current state, without +triggering an enumeration. -If `tc` has not been run, the returned word graph reflects whatever -incomplete state has been built so far. +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) +@cxxdereference current_word_graph(tc::ToddCoxeter) = LibSemigroups.current_word_graph(tc) """ word_graph(tc::ToddCoxeter) -> WordGraph -Return the underlying word graph of `tc`, after running the algorithm to -completion and standardizing. +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. -This function triggers a full enumeration of `tc`, which may never -terminate. +!!! warning + This function may never terminate if the congruence is undecidable. # See also @@ -598,10 +659,14 @@ terminate. """ index_of(tc::ToddCoxeter, w::AbstractVector{<:Integer}) -> Int -Return the 1-based index of the congruence class containing `w`. +Return the 1-based index of the congruence class containing `w`, triggering +a full enumeration of `tc`. -This function triggers a full enumeration of `tc`, which may never -terminate. +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 @@ -614,6 +679,9 @@ terminate. - `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), @@ -632,7 +700,10 @@ 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). +[`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 @@ -689,7 +760,13 @@ 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 a run. +(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 @@ -712,6 +789,28 @@ function current_word_of(tc::ToddCoxeter, i::Integer) 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 # ============================================================================ @@ -725,6 +824,12 @@ 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. @@ -734,9 +839,10 @@ number_of_classes(tc::ToddCoxeter) = LibSemigroups.number_of_classes(tc) """ kind(tc::ToddCoxeter) -> congruence_kind -Return the kind of the congruence represented by `tc` — either -[`twosided`](@ref Semigroups.twosided) or -[`onesided`](@ref Semigroups.onesided). +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) @@ -754,11 +860,13 @@ number_of_generating_pairs(tc::ToddCoxeter) = """ generating_pairs(tc::ToddCoxeter) -> Vector{Tuple{Vector{Int}, Vector{Int}}} -Return the generating pairs of `tc` as 1-based word pairs. +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. +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) @@ -772,7 +880,12 @@ end """ presentation(tc::ToddCoxeter) -> Presentation -Return a copy of the presentation used by `tc`. +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) @@ -804,8 +917,7 @@ Create an independent copy of `tc`. """ Base.copy(tc::ToddCoxeter) = LibSemigroups.ToddCoxeterWord(tc) -Base.deepcopy_internal(tc::ToddCoxeter, ::IdDict) = - LibSemigroups.ToddCoxeterWord(tc) +Base.deepcopy_internal(tc::ToddCoxeter, ::IdDict) = LibSemigroups.ToddCoxeterWord(tc) # ============================================================================ # Free functions (todd_coxeter:: namespace) @@ -815,7 +927,7 @@ Base.deepcopy_internal(tc::ToddCoxeter, ::IdDict) = is_non_trivial(tc::ToddCoxeter; tries::Integer = 10, try_for::TimePeriod = Dates.Millisecond(100), - threshold::Real = 0.99) -> [`tril`](@ref Semigroups.tril) + threshold::Real = 0.99) -> tril Heuristically check whether the congruence represented by `tc` is non-trivial. @@ -850,13 +962,13 @@ function is_non_trivial( try_for::TimePeriod = Dates.Millisecond(100), threshold::Real = 0.99, ) - ns = convert(Nanosecond, try_for) - return @wrap_libsemigroups_call LibSemigroups.tc_is_non_trivial( - tc, - UInt(tries), - Int64(Dates.value(ns)), - Float32(threshold), - ) + # 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 """ @@ -884,15 +996,13 @@ if no redundant rule is identified within the timeout. # See also -[`redundant_rule`](@ref Semigroups.redundant_rule) — the analogous +[`redundant_rule`](@ref Semigroups.redundant_rule) - the analogous Knuth-Bendix-based helper. """ function tc_redundant_rule(p::Presentation, timeout::TimePeriod) - ns = convert(Nanosecond, timeout) - idx = @wrap_libsemigroups_call LibSemigroups.tc_redundant_rule( - p, - Int64(Dates.value(ns)), - ) + # 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 diff --git a/test/test_todd_coxeter.jl b/test/test_todd_coxeter.jl index db80d5f..77f7107 100644 --- a/test/test_todd_coxeter.jl +++ b/test/test_todd_coxeter.jl @@ -5,105 +5,69 @@ """ test_todd_coxeter.jl - Tests for ToddCoxeter -Phase 3b of the v1 design. Ports a focused subset of [quick] cases from -libsemigroups/tests/test-todd-coxeter.cpp, plus binding-surface and high-level -integration tests. - -This file is brought in **before** the Julia wrapper for ToddCoxeter is fully -implemented — by design. Many assertions will fail with `MethodError` or -`UndefVarError` until Stage 3 implements the wrapper methods one slice at a -time. The constructor and `number_of_classes` work directly off the C++ glue -and so a handful of assertions may already pass. +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 -# ToddCoxeter and the enum constants are not yet exported from `Semigroups` -# (Stage 4 adds the exports). Bring them into local scope under their public -# names for readability. -const ToddCoxeter = Semigroups.ToddCoxeter - -const strategy_hlt = Semigroups.strategy_hlt -const strategy_felsch = Semigroups.strategy_felsch -const strategy_CR = Semigroups.strategy_CR -const strategy_R_over_C = Semigroups.strategy_R_over_C -const strategy_Cr = Semigroups.strategy_Cr -const strategy_Rc = Semigroups.strategy_Rc - -const lookahead_extent_full = Semigroups.lookahead_extent_full -const lookahead_extent_partial = Semigroups.lookahead_extent_partial - -const lookahead_style_hlt = Semigroups.lookahead_style_hlt -const lookahead_style_felsch = Semigroups.lookahead_style_felsch - -const def_policy_no_stack_if_no_space = Semigroups.def_policy_no_stack_if_no_space -const def_policy_purge_from_top = Semigroups.def_policy_purge_from_top -const def_policy_purge_all = Semigroups.def_policy_purge_all -const def_policy_discard_all_if_no_space = Semigroups.def_policy_discard_all_if_no_space -const def_policy_unlimited = Semigroups.def_policy_unlimited - -const def_version_one = Semigroups.def_version_one -const def_version_two = Semigroups.def_version_two - -# ---- Word conversion helpers (1-based Julia <-> 0-based C++ in libsemigroups) ---- - -# Mirror the KB pattern: `_test_todd_coxeter_cword(0, 0, 1)` builds the Julia -# word `[1, 1, 2]`. Use a long prefix matching the file name so other test -# files included into the same module by `runtests.jl` cannot accidentally -# shadow this helper. -_test_todd_coxeter_cword(xs::Integer...) = [Int(x) + 1 for x in xs] - -# ============================================================================ -# Layer 1 — binding-surface tests -# ============================================================================ +# 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{Semigroups.congruence_kind, Presentation}) - @test hasmethod(ToddCoxeter, Tuple{Semigroups.congruence_kind, ToddCoxeter}) - @test hasmethod(ToddCoxeter, Tuple{Semigroups.congruence_kind, WordGraph}) + @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, Semigroups.congruence_kind, Presentation}) - @test hasmethod(init!, Tuple{ToddCoxeter, Semigroups.congruence_kind, ToddCoxeter}) - @test hasmethod(init!, Tuple{ToddCoxeter, Semigroups.congruence_kind, WordGraph}) + @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(Semigroups.strategy, Tuple{ToddCoxeter}) - @test hasmethod(Semigroups.strategy!, Tuple{ToddCoxeter, typeof(strategy_hlt)}) - @test hasmethod(Semigroups.lookahead_extent, Tuple{ToddCoxeter}) - @test hasmethod(Semigroups.lookahead_extent!, Tuple{ToddCoxeter, typeof(lookahead_extent_full)}) - @test hasmethod(Semigroups.lookahead_style, Tuple{ToddCoxeter}) - @test hasmethod(Semigroups.lookahead_style!, Tuple{ToddCoxeter, typeof(lookahead_style_hlt)}) - @test hasmethod(Semigroups.save, Tuple{ToddCoxeter}) - @test hasmethod(Semigroups.save!, Tuple{ToddCoxeter, Bool}) - @test hasmethod(Semigroups.use_relations_in_extra, Tuple{ToddCoxeter}) - @test hasmethod(Semigroups.use_relations_in_extra!, Tuple{ToddCoxeter, Bool}) - @test hasmethod(Semigroups.lower_bound, Tuple{ToddCoxeter}) - @test hasmethod(Semigroups.lower_bound!, Tuple{ToddCoxeter, Integer}) - @test hasmethod(Semigroups.def_version, Tuple{ToddCoxeter}) - @test hasmethod(Semigroups.def_version!, Tuple{ToddCoxeter, typeof(def_version_one)}) - @test hasmethod(Semigroups.def_policy, Tuple{ToddCoxeter}) - @test hasmethod(Semigroups.def_policy!, Tuple{ToddCoxeter, typeof(def_policy_purge_all)}) + @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(Semigroups.is_standardized, Tuple{ToddCoxeter}) - @test hasmethod(Semigroups.is_standardized, Tuple{ToddCoxeter, Order}) - @test hasmethod(Semigroups.current_word_graph, Tuple{ToddCoxeter}) + @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(Semigroups.index_of, Tuple{ToddCoxeter, AbstractVector{<:Integer}}) - @test hasmethod(Semigroups.current_index_of, Tuple{ToddCoxeter, AbstractVector{<:Integer}}) - @test hasmethod(Semigroups.word_of, Tuple{ToddCoxeter, Integer}) - @test hasmethod(Semigroups.current_word_of, Tuple{ToddCoxeter, Integer}) + @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}) @@ -113,26 +77,36 @@ _test_todd_coxeter_cword(xs::Integer...) = [Int(x) + 1 for x in xs] @test hasmethod(presentation, Tuple{ToddCoxeter}) # Free functions - @test hasmethod(Semigroups.is_non_trivial, Tuple{ToddCoxeter}) - @test hasmethod(Semigroups.tc_redundant_rule, Tuple{Presentation, TimePeriod}) + @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.show, Tuple{IO,ToddCoxeter}) @test hasmethod(Base.copy, Tuple{ToddCoxeter}) - # Inherited from CongruenceCommon (already wrapped in src/cong-common.jl) - @test hasmethod(add_generating_pair!, Tuple{Semigroups.CongruenceCommon, AbstractVector{<:Integer}, AbstractVector{<:Integer}}) - @test hasmethod(currently_contains, Tuple{Semigroups.CongruenceCommon, AbstractVector{<:Integer}, AbstractVector{<:Integer}}) - @test hasmethod(Semigroups.contains, Tuple{Semigroups.CongruenceCommon, AbstractVector{<:Integer}, AbstractVector{<:Integer}}) - @test hasmethod(Semigroups.reduce, Tuple{Semigroups.CongruenceCommon, AbstractVector{<:Integer}}) - @test hasmethod(reduce_no_run, Tuple{Semigroups.CongruenceCommon, AbstractVector{<:Integer}}) - @test hasmethod(normal_forms, Tuple{Semigroups.CongruenceCommon}) - @test hasmethod(non_trivial_classes, Tuple{Semigroups.CongruenceCommon, Semigroups.CongruenceCommon}) + # 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 # ============================================================================ -# Layer 2 — correctness (ported from test-todd-coxeter.cpp) +# correctness tests inspired by test-todd-coxeter.cpp # ============================================================================ @testset "TC000 - small 2-sided congruence (27 classes)" begin @@ -140,18 +114,16 @@ end # 2-generator semigroup, rules: 000 = 0, 1111 = 1, 0101 = 00. p = Presentation() set_alphabet!(p, 2) - add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) - add_rule_no_checks!(p, _test_todd_coxeter_cword(1, 1, 1, 1), _test_todd_coxeter_cword(1)) - add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 1, 0, 1), _test_todd_coxeter_cword(0, 0)) + 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 + normal_forms count matches number_of_classes standardize!(tc, ORDER_SHORTLEX) - nfs = normal_forms(tc) - @test length(nfs) == 27 + @test length(normal_forms(tc)) == 27 end @testset "TC001 - small 2-sided congruence (5 classes)" begin @@ -159,8 +131,8 @@ end # 2-generator semigroup, rules: 000 = 0, 0 = 11. p = Presentation() set_alphabet!(p, 2) - add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) - add_rule_no_checks!(p, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1, 1)) + 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) @@ -168,133 +140,122 @@ end @test finished(tc) # Index-of: 001 == 00001 (1-based: [1,1,2] == [1,1,1,1,2]) - @test Semigroups.index_of(tc, _test_todd_coxeter_cword(0, 0, 1)) == - Semigroups.index_of(tc, _test_todd_coxeter_cword(0, 0, 0, 0, 1)) - @test Semigroups.index_of(tc, _test_todd_coxeter_cword(0, 1, 1, 0, 0, 1)) == - Semigroups.index_of(tc, _test_todd_coxeter_cword(0, 0, 0, 0, 1)) - @test Semigroups.index_of(tc, _test_todd_coxeter_cword(0, 0, 0)) != - Semigroups.index_of(tc, _test_todd_coxeter_cword(1)) + @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 Semigroups.word_of(tc, 1) == _test_todd_coxeter_cword(0) # C++ index 0 - @test Semigroups.word_of(tc, 2) == _test_todd_coxeter_cword(1) # C++ index 1 - @test Semigroups.word_of(tc, 3) == _test_todd_coxeter_cword(0, 0) # C++ index 2 + @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 Semigroups.is_standardized(tc, ORDER_LEX) - @test Semigroups.is_standardized(tc) - @test !Semigroups.is_standardized(tc, ORDER_SHORTLEX) - - @test Semigroups.word_of(tc, 1) == _test_todd_coxeter_cword(0) # 0 - @test Semigroups.word_of(tc, 2) == _test_todd_coxeter_cword(0, 0) # 00 - @test Semigroups.word_of(tc, 3) == _test_todd_coxeter_cword(0, 0, 1) # 001 - @test Semigroups.word_of(tc, 4) == _test_todd_coxeter_cword(0, 0, 1, 0) # 0010 - @test Semigroups.word_of(tc, 5) == _test_todd_coxeter_cword(1) # 1 - - # word_of/index_of round-trip (1-based on the Julia side) - for i in 1:5 - @test Semigroups.index_of(tc, Semigroups.word_of(tc, i)) == i + @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 for shortlex again, and check normal_forms equals expected. standardize!(tc, ORDER_SHORTLEX) - @test Semigroups.is_standardized(tc, ORDER_SHORTLEX) - @test normal_forms(tc) == [ - _test_todd_coxeter_cword(0), - _test_todd_coxeter_cword(1), - _test_todd_coxeter_cword(0, 0), - _test_todd_coxeter_cword(0, 1), - _test_todd_coxeter_cword(0, 0, 1), - ] + @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, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) - add_rule_no_checks!(p, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1, 1)) + 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, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(0, 0)) + 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, so mirror that and - # add a couple of cheap positive observations about the resulting object's - # state. The 1-node, 2-out-degree WordGraph has all-UNDEFINED targets, so - # the only thing safe to assert about `number_of_classes` is that it is a - # non-negative integer. + # 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 - # Runner-state queries should not throw on a freshly constructed object. @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, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) - add_rule_no_checks!(p, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1, 1)) + 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) - Semigroups.strategy!(tc, strategy_felsch) - @test Semigroups.strategy(tc) == strategy_felsch + strategy!(tc, strategy_felsch) + @test strategy(tc) == strategy_felsch - Semigroups.lookahead_extent!(tc, lookahead_extent_full) - @test Semigroups.lookahead_extent(tc) == lookahead_extent_full + lookahead_extent!(tc, lookahead_extent_full) + @test lookahead_extent(tc) == lookahead_extent_full - Semigroups.lookahead_style!(tc, lookahead_style_felsch) - @test Semigroups.lookahead_style(tc) == lookahead_style_felsch + lookahead_style!(tc, lookahead_style_felsch) + @test lookahead_style(tc) == lookahead_style_felsch - Semigroups.save!(tc, true) - @test Semigroups.save(tc) == true + save!(tc, true) + @test save(tc) == true - Semigroups.use_relations_in_extra!(tc, false) - @test Semigroups.use_relations_in_extra(tc) == false + use_relations_in_extra!(tc, false) + @test use_relations_in_extra(tc) == false - Semigroups.lower_bound!(tc, 5) - @test Semigroups.lower_bound(tc) == 5 + # 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 - Semigroups.def_version!(tc, def_version_two) - @test Semigroups.def_version(tc) == def_version_two + def_version!(tc, def_version_two) + @test def_version(tc) == def_version_two - Semigroups.def_policy!(tc, def_policy_purge_all) - @test Semigroups.def_policy(tc) == def_policy_purge_all + def_policy!(tc, def_policy_purge_all) + @test def_policy(tc) == def_policy_purge_all end @testset "TC - current_word_graph after run!" begin - # TC1 (5 classes). 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 that is not a real class - # representative when the presentation does NOT contain the empty word). + # 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, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) - add_rule_no_checks!(p, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1, 1)) + 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 = Semigroups.current_word_graph(tc) - # current_word_graph returns a CxxBaseRef{WordGraph}; check usability via - # number_of_nodes (which accepts either WordGraph or its CxxBaseRef). + + cwg = current_word_graph(tc) @test number_of_nodes(cwg) >= number_of_classes(tc) + 1 - # word_graph(tc) returns the standardized, pruned graph; for a - # presentation without the empty word, this is exactly classes + 1. - swg = Semigroups.word_graph(tc) + swg = word_graph(tc) @test number_of_nodes(swg) == number_of_classes(tc) + 1 end @@ -304,114 +265,103 @@ end p = Presentation() set_alphabet!(p, 1) tc = ToddCoxeter(twosided, p) - @test Semigroups.is_non_trivial(tc) == tril_TRUE + @test is_non_trivial(tc) == tril_TRUE end @testset "TC - tc_redundant_rule" begin - # Irredundant: TC103-style presentation (single rule on a 1-letter alphabet - # cannot be redundant). p = Presentation() set_alphabet!(p, 2) - add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) - add_rule_no_checks!(p, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1, 1)) - @test Semigroups.tc_redundant_rule(p, Millisecond(50)) === nothing + 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 - # Trivially redundant: add a duplicate rule. - add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) - idx = Semigroups.tc_redundant_rule(p, Millisecond(100)) + # 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, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) - add_rule_no_checks!(p, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1, 1)) + 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; should agree with index_of equality - @test Semigroups.contains(tc, _test_todd_coxeter_cword(0, 0, 1), _test_todd_coxeter_cword(0, 0, 0, 0, 1)) - @test currently_contains(tc, _test_todd_coxeter_cword(0, 0, 1), _test_todd_coxeter_cword(0, 0, 0, 0, 1)) == - tril_TRUE + # 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 - # reduce returns the standardized representative - r = Semigroups.reduce(tc, _test_todd_coxeter_cword(0, 0, 0, 0, 1)) + r = Semigroups.reduce(tc, _tc_word(0, 0, 0, 0, 1)) @test r isa Vector{Int} - @test Semigroups.contains(tc, r, _test_todd_coxeter_cword(0, 0, 0, 0, 1)) + @test Semigroups.contains(tc, r, _tc_word(0, 0, 0, 0, 1)) - # normal_forms count == number_of_classes - nfs = normal_forms(tc) - @test length(nfs) == number_of_classes(tc) + @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, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) - add_rule_no_checks!(p, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1, 1)) + 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 is a quotient of tc1 collapsing 0 ~ 1; should produce a smaller - # number_of_classes. + # tc2 collapses 0 ~ 1 -> strictly fewer classes than tc1. tc2 = ToddCoxeter(twosided, p) - add_generating_pair!(tc2, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1)) + 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 -# ============================================================================ -# Layer 3 — high-level integration -# ============================================================================ - @testset "ToddCoxeter high-level Julia API" begin p = Presentation() set_alphabet!(p, 2) - add_rule_no_checks!(p, _test_todd_coxeter_cword(0, 0, 0), _test_todd_coxeter_cword(0)) - add_rule_no_checks!(p, _test_todd_coxeter_cword(0), _test_todd_coxeter_cword(1, 1)) + 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. Mutate one setting on the copy. + # copy: independent objects. tc2 = copy(tc) @test length(tc2) == length(tc) - Semigroups.strategy!(tc2, strategy_felsch) - @test Semigroups.strategy(tc2) == strategy_felsch + strategy!(tc2, strategy_felsch) + @test strategy(tc2) == strategy_felsch - # 1-based round-trip for index_of / word_of + # 1-based round-trip for index_of / word_of. standardize!(tc, ORDER_SHORTLEX) - for i in 1:Int(number_of_classes(tc)) - @test Semigroups.index_of(tc, Semigroups.word_of(tc, i)) == i + for i = 1:Int(number_of_classes(tc)) + @test index_of(tc, word_of(tc, i)) == i end - # Setter chaining (chained left-to-right; both effects must persist). - Semigroups.save!(Semigroups.strategy!(tc, strategy_felsch), false) - @test Semigroups.strategy(tc) == strategy_felsch - @test Semigroups.save(tc) == false + # 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 + # standardize! returns a Bool. fresh = ToddCoxeter(twosided, p) run!(fresh) @test standardize!(fresh, ORDER_SHORTLEX) isa Bool end # ============================================================================ -# TODO — deferred test cases (re-port when their dependencies land) +# TODO - port full test-todd-coxeter.cpp test cases # ============================================================================ -# - class_by_index / class_of (deferred; needs Paths-with-alphabet-transform) -# - to<>(tc) conversions (Phase 4b: to(tc), to(tc), -# to(...), to(tc)) -# - Numeric setters (def_max, f_defs, hlt_defs, large_collapse, lookahead_* -# numerics, lookbehind_threshold) — v1.1 -# - perform_lookahead / perform_lookahead_for / perform_lookahead_until / -# perform_lookbehind member callbacks — v1.1 -# - All [extreme]-tagged libsemigroups tests -# - Tests that depend on `presentation::examples::*` not yet exercised -# - RewriteFromLeft-related KB cross-comparisons -# - shrink_to_fit (not yet bound) From 265053363ce2030caf71907dd5ab95e9dfa83179 Mon Sep 17 00:00:00 2001 From: James Swent Date: Mon, 27 Apr 2026 11:13:50 +0100 Subject: [PATCH 17/17] fix: route is_standardized trough current_word_graph --- deps/src/CMakeLists.txt | 4 ++++ deps/src/todd-coxeter.cpp | 10 +++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/deps/src/CMakeLists.txt b/deps/src/CMakeLists.txt index 0a8ead7..4f33dda 100644 --- a/deps/src/CMakeLists.txt +++ b/deps/src/CMakeLists.txt @@ -85,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/todd-coxeter.cpp b/deps/src/todd-coxeter.cpp index cdb9e8a..bd259da 100644 --- a/deps/src/todd-coxeter.cpp +++ b/deps/src/todd-coxeter.cpp @@ -214,11 +214,15 @@ namespace libsemigroups_julia { return self.standardize(ord); }); - type.method("is_standardized", - [](TC const& self) -> bool { return self.is_standardized(); }); + // 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.is_standardized(ord); + return self.current_word_graph().is_standardized(ord); }); // current_word_graph: large stable data, return by const reference.