diff --git a/deps/src/CMakeLists.txt b/deps/src/CMakeLists.txt index 6bc4c3a..37a9edd 100644 --- a/deps/src/CMakeLists.txt +++ b/deps/src/CMakeLists.txt @@ -46,6 +46,8 @@ add_library(libsemigroups_julia SHARED libsemigroups_julia.cpp bmat8.cpp constants.cpp + froidure-pin-base.cpp + froidure-pin.cpp order.cpp report.cpp runner.cpp diff --git a/deps/src/froidure-pin-base.cpp b/deps/src/froidure-pin-base.cpp new file mode 100644 index 0000000..50a0eb5 --- /dev/null +++ b/deps/src/froidure-pin-base.cpp @@ -0,0 +1,408 @@ +// froidure-pin-base.cpp - FroidurePinBase bindings for libsemigroups_julia +// +// 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 . +// +// This file exposes the libsemigroups FroidurePinBase class to Julia via +// CxxWrap. FroidurePinBase is an abstract base class inheriting from Runner +// that provides non-element-specific member functions for FroidurePin. + +// CRITICAL: libsemigroups_julia.hpp MUST be included first (fmt consteval fix) +#include "libsemigroups_julia.hpp" + +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace jlcxx { + template <> + struct IsMirroredType : std::false_type {}; + + template <> + struct SuperType { + using type = libsemigroups::Runner; + }; +} // namespace jlcxx + +namespace libsemigroups_julia { + + void define_froidure_pin_base(jl::Module& m) { + using libsemigroups::FroidurePinBase; + using libsemigroups::Runner; + using libsemigroups::word_type; + + // Register FroidurePinBase inheriting from Runner. + // No constructors — this is an abstract base class only instantiated + // through FroidurePin. + auto type = m.add_type("FroidurePinBase", + jlcxx::julia_base_type()); + + //////////////////////////////////////////////////////////////////////// + // Settings (getter/setter split per CxxWrap convention) + //////////////////////////////////////////////////////////////////////// + + // batch_size - Returns the current batch size + type.method("batch_size", [](FroidurePinBase const& self) -> size_t { + return self.batch_size(); + }); + + // set_batch_size! - Sets the batch size + type.method("set_batch_size!", [](FroidurePinBase& self, size_t val) { + self.batch_size(val); + }); + + //////////////////////////////////////////////////////////////////////// + // Size / enumeration + //////////////////////////////////////////////////////////////////////// + + // size - Returns the total number of elements (triggers full enumeration) + type.method("size", + [](FroidurePinBase& self) -> size_t { return self.size(); }); + + // current_size - Returns elements enumerated so far (no enumeration) + type.method("current_size", [](FroidurePinBase const& self) -> size_t { + return self.current_size(); + }); + + // degree - Returns the degree of the elements + type.method("degree", [](FroidurePinBase const& self) -> size_t { + return self.degree(); + }); + + // number_of_generators - Returns the number of generators + type.method("number_of_generators", + [](FroidurePinBase const& self) -> size_t { + return self.number_of_generators(); + }); + + // enumerate! - Enumerate until at least `limit` elements are found + type.method("enumerate!", [](FroidurePinBase& self, size_t limit) { + self.enumerate(limit); + }); + + // number_of_rules - Total number of rules (triggers full enumeration) + type.method("number_of_rules", [](FroidurePinBase& self) -> size_t { + return self.number_of_rules(); + }); + + // current_number_of_rules - Rules found so far (no enumeration) + type.method("current_number_of_rules", + [](FroidurePinBase const& self) -> size_t { + return self.current_number_of_rules(); + }); + + // current_max_word_length - Max word length so far (no enumeration) + type.method("current_max_word_length", + [](FroidurePinBase const& self) -> size_t { + return self.current_max_word_length(); + }); + + // contains_one - Is the identity an element? (triggers full enumeration) + type.method("contains_one", [](FroidurePinBase& self) -> bool { + return self.contains_one(); + }); + + // currently_contains_one - Is the identity known to be an element? + type.method("currently_contains_one", + [](FroidurePinBase const& self) -> bool { + return self.currently_contains_one(); + }); + + // number_of_elements_of_length (one-arg) - Elements with given length + type.method("number_of_elements_of_length", + [](FroidurePinBase const& self, size_t len) -> size_t { + return self.number_of_elements_of_length(len); + }); + + // number_of_elements_of_length (two-arg) - Elements with length in + // [min, max) + type.method( + "number_of_elements_of_length_range", + [](FroidurePinBase const& self, size_t min, size_t max) -> size_t { + return self.number_of_elements_of_length(min, max); + }); + + //////////////////////////////////////////////////////////////////////// + // Index queries — checked and _no_checks variants + //////////////////////////////////////////////////////////////////////// + + // prefix / prefix_no_checks + type.method("prefix", + [](FroidurePinBase const& self, uint32_t pos) -> uint32_t { + return self.prefix(pos); + }); + type.method("prefix_no_checks", + [](FroidurePinBase const& self, uint32_t pos) -> uint32_t { + return self.prefix_no_checks(pos); + }); + + // suffix / suffix_no_checks + type.method("suffix", + [](FroidurePinBase const& self, uint32_t pos) -> uint32_t { + return self.suffix(pos); + }); + type.method("suffix_no_checks", + [](FroidurePinBase const& self, uint32_t pos) -> uint32_t { + return self.suffix_no_checks(pos); + }); + + // first_letter / first_letter_no_checks + type.method("first_letter", + [](FroidurePinBase const& self, uint32_t pos) -> uint32_t { + return self.first_letter(pos); + }); + type.method("first_letter_no_checks", + [](FroidurePinBase const& self, uint32_t pos) -> uint32_t { + return self.first_letter_no_checks(pos); + }); + + // final_letter / final_letter_no_checks + type.method("final_letter", + [](FroidurePinBase const& self, uint32_t pos) -> uint32_t { + return self.final_letter(pos); + }); + type.method("final_letter_no_checks", + [](FroidurePinBase const& self, uint32_t pos) -> uint32_t { + return self.final_letter_no_checks(pos); + }); + + // current_length / current_length_no_checks + type.method("current_length", + [](FroidurePinBase const& self, uint32_t pos) -> size_t { + return self.current_length(pos); + }); + type.method("current_length_no_checks", + [](FroidurePinBase const& self, uint32_t pos) -> size_t { + return self.current_length_no_checks(pos); + }); + + // length / length_no_checks (trigger full enumeration) + type.method("length", [](FroidurePinBase& self, uint32_t pos) -> size_t { + return self.length(pos); + }); + type.method("length_no_checks", + [](FroidurePinBase& self, uint32_t pos) -> size_t { + return self.length_no_checks(pos); + }); + + // position_of_generator / position_of_generator_no_checks + type.method("position_of_generator", + [](FroidurePinBase const& self, uint32_t i) -> uint32_t { + return self.position_of_generator(i); + }); + type.method("position_of_generator_no_checks", + [](FroidurePinBase const& self, uint32_t i) -> uint32_t { + return self.position_of_generator_no_checks(i); + }); + + //////////////////////////////////////////////////////////////////////// + // Factorisation — froidure_pin:: free functions returning word_type + //////////////////////////////////////////////////////////////////////// + + // current_minimal_factorisation (checked, no enumeration) + m.method( + "current_minimal_factorisation", + [](FroidurePinBase const& fpb, uint32_t pos) -> word_type { + return libsemigroups::froidure_pin::current_minimal_factorisation( + fpb, pos); + }); + + // current_minimal_factorisation_no_checks (unchecked, no enumeration) + m.method("current_minimal_factorisation_no_checks", + [](FroidurePinBase const& fpb, uint32_t pos) -> word_type { + return libsemigroups::froidure_pin:: + current_minimal_factorisation_no_checks(fpb, pos); + }); + + // minimal_factorisation (checked, triggers partial enumeration) + m.method("minimal_factorisation", + [](FroidurePinBase& fpb, uint32_t pos) -> word_type { + return libsemigroups::froidure_pin::minimal_factorisation(fpb, + pos); + }); + + // factorisation (checked, triggers partial enumeration) + m.method("factorisation", + [](FroidurePinBase& fpb, uint32_t pos) -> word_type { + return libsemigroups::froidure_pin::factorisation(fpb, pos); + }); + + //////////////////////////////////////////////////////////////////////// + // Word-position queries — ArrayRef for Julia Vector{UInt} + //////////////////////////////////////////////////////////////////////// + + // froidure_pin::current_position (checked, no enumeration) + m.method("current_position", + [](FroidurePinBase const& fpb, + jlcxx::ArrayRef arr) -> uint32_t { + word_type w(arr.begin(), arr.end()); + return libsemigroups::froidure_pin::current_position(fpb, w); + }); + + // froidure_pin::current_position_no_checks (unchecked, no enumeration) + m.method("current_position_no_checks", + [](FroidurePinBase const& fpb, + jlcxx::ArrayRef arr) -> uint32_t { + word_type w(arr.begin(), arr.end()); + return libsemigroups::froidure_pin::current_position_no_checks( + fpb, w); + }); + + // froidure_pin::position (checked, triggers full enumeration) + m.method("position", + [](FroidurePinBase& fpb, jlcxx::ArrayRef arr) -> uint32_t { + word_type w(arr.begin(), arr.end()); + return libsemigroups::froidure_pin::position(fpb, w); + }); + + // froidure_pin::position_no_checks (unchecked, triggers full enumeration) + m.method("position_no_checks", + [](FroidurePinBase& fpb, jlcxx::ArrayRef arr) -> uint32_t { + word_type w(arr.begin(), arr.end()); + return libsemigroups::froidure_pin::position_no_checks(fpb, w); + }); + + // froidure_pin::product_by_reduction (checked) + m.method( + "product_by_reduction", + [](FroidurePinBase const& fpb, uint32_t i, uint32_t j) -> uint32_t { + return libsemigroups::froidure_pin::product_by_reduction(fpb, i, j); + }); + + // froidure_pin::product_by_reduction_no_checks (unchecked) + m.method( + "product_by_reduction_no_checks", + [](FroidurePinBase const& fpb, uint32_t i, uint32_t j) -> uint32_t { + return libsemigroups::froidure_pin::product_by_reduction_no_checks( + fpb, i, j); + }); + + //////////////////////////////////////////////////////////////////////// + // Cayley graphs — return const& to already-bound WordGraph + //////////////////////////////////////////////////////////////////////// + + // right_cayley_graph (triggers full enumeration) + type.method( + "right_cayley_graph", + [](FroidurePinBase& self) -> FroidurePinBase::cayley_graph_type const& { + return self.right_cayley_graph(); + }); + + // current_right_cayley_graph (no enumeration) + type.method("current_right_cayley_graph", + [](FroidurePinBase const& self) + -> FroidurePinBase::cayley_graph_type const& { + return self.current_right_cayley_graph(); + }); + + // left_cayley_graph (triggers full enumeration) + type.method( + "left_cayley_graph", + [](FroidurePinBase& self) -> FroidurePinBase::cayley_graph_type const& { + return self.left_cayley_graph(); + }); + + // current_left_cayley_graph (no enumeration) + type.method("current_left_cayley_graph", + [](FroidurePinBase const& self) + -> FroidurePinBase::cayley_graph_type const& { + return self.current_left_cayley_graph(); + }); + + //////////////////////////////////////////////////////////////////////// + // Materialized collections — rules and normal forms + //////////////////////////////////////////////////////////////////////// + // const_rule_iterator dereferences to relation_type = + // std::pair + // CxxWrap cannot return std::pair, so we split into two parallel + // vectors: rules_lhs and rules_rhs. + + // rules_lhs / rules_rhs — full enumeration, then collect + m.method("rules_lhs", [](FroidurePinBase& fpb) -> std::vector { + std::vector result; + auto it = fpb.cbegin_rules(); + auto end = fpb.cend_rules(); + for (; it != end; ++it) { + result.push_back((*it).first); + } + return result; + }); + + m.method("rules_rhs", [](FroidurePinBase& fpb) -> std::vector { + std::vector result; + auto it = fpb.cbegin_rules(); + auto end = fpb.cend_rules(); + for (; it != end; ++it) { + result.push_back((*it).second); + } + return result; + }); + + // current_rules_lhs / current_rules_rhs — no enumeration + m.method("current_rules_lhs", + [](FroidurePinBase const& fpb) -> std::vector { + std::vector result; + auto it = fpb.cbegin_current_rules(); + auto end = fpb.cend_current_rules(); + for (; it != end; ++it) { + result.push_back((*it).first); + } + return result; + }); + + m.method("current_rules_rhs", + [](FroidurePinBase const& fpb) -> std::vector { + std::vector result; + auto it = fpb.cbegin_current_rules(); + auto end = fpb.cend_current_rules(); + for (; it != end; ++it) { + result.push_back((*it).second); + } + return result; + }); + + // normal_forms — full enumeration, collect into vector + m.method("normal_forms", + [](FroidurePinBase& fpb) -> std::vector { + std::vector result; + auto it = fpb.cbegin_normal_forms(); + auto end = fpb.cend_normal_forms(); + for (; it != end; ++it) { + result.push_back(*it); + } + return result; + }); + + // current_normal_forms — no enumeration + m.method("current_normal_forms", + [](FroidurePinBase const& fpb) -> std::vector { + std::vector result; + auto it = fpb.cbegin_current_normal_forms(); + auto end = fpb.cend_current_normal_forms(); + for (; it != end; ++it) { + result.push_back(*it); + } + return result; + }); + } + +} // namespace libsemigroups_julia diff --git a/deps/src/froidure-pin.cpp b/deps/src/froidure-pin.cpp new file mode 100644 index 0000000..f7ba4c9 --- /dev/null +++ b/deps/src/froidure-pin.cpp @@ -0,0 +1,370 @@ +// froidure-pin.cpp - FroidurePin bindings for libsemigroups_julia +// +// 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 . +// +// This file exposes the libsemigroups FroidurePin template class to Julia +// via CxxWrap for all 10 element types (Transf1/2/4, PPerm1/2/4, Perm1/2/4, +// BMat8). FroidurePin inherits from FroidurePinBase. + +// CRITICAL: libsemigroups_julia.hpp MUST be included first (fmt consteval fix) +#include "libsemigroups_julia.hpp" + +#include +#include +#include + +#include + +#include +#include +#include +#include + +//////////////////////////////////////////////////////////////////////// +// CxxWrap type traits — IsMirroredType and SuperType specializations +//////////////////////////////////////////////////////////////////////// + +namespace jlcxx { + + // IsMirroredType — one per concrete FroidurePin instantiation + template <> + struct IsMirroredType< + libsemigroups::FroidurePin>> + : std::false_type {}; + template <> + struct IsMirroredType< + libsemigroups::FroidurePin>> + : std::false_type {}; + template <> + struct IsMirroredType< + libsemigroups::FroidurePin>> + : std::false_type {}; + + template <> + struct IsMirroredType< + libsemigroups::FroidurePin>> + : std::false_type {}; + template <> + struct IsMirroredType< + libsemigroups::FroidurePin>> + : std::false_type {}; + template <> + struct IsMirroredType< + libsemigroups::FroidurePin>> + : std::false_type {}; + + template <> + struct IsMirroredType< + libsemigroups::FroidurePin>> + : std::false_type {}; + template <> + struct IsMirroredType< + libsemigroups::FroidurePin>> + : std::false_type {}; + template <> + struct IsMirroredType< + libsemigroups::FroidurePin>> + : std::false_type {}; + + template <> + struct IsMirroredType> + : std::false_type {}; + + // SuperType — partial specialization for all FroidurePin + template + struct SuperType> { + using type = libsemigroups::FroidurePinBase; + }; + +} // namespace jlcxx + +namespace libsemigroups_julia { + + namespace { + + //////////////////////////////////////////////////////////////////////// + // bind_froidure_pin — template helper that registers all + // element-typed methods for a single FroidurePin instantiation. + //////////////////////////////////////////////////////////////////////// + + template + void bind_froidure_pin(jl::Module& m, std::string const& name) { + using FP = libsemigroups::FroidurePin; + using libsemigroups::FroidurePinBase; + using libsemigroups::word_type; + + auto type + = m.add_type(name, jlcxx::julia_base_type()); + + //////////////////////////////////////////////////////////////////// + // 1. Constructors — 1-4 generator arg lambdas + // (Can't construct StdVector of wrapped types Julia-side) + //////////////////////////////////////////////////////////////////// + + m.method(name, [](E const& g1) { + std::vector v{g1}; + return FP(v.begin(), v.end()); + }); + + m.method(name, [](E const& g1, E const& g2) { + std::vector v{g1, g2}; + return FP(v.begin(), v.end()); + }); + + m.method(name, [](E const& g1, E const& g2, E const& g3) { + std::vector v{g1, g2, g3}; + return FP(v.begin(), v.end()); + }); + + m.method(name, [](E const& g1, E const& g2, E const& g3, E const& g4) { + std::vector v{g1, g2, g3, g4}; + return FP(v.begin(), v.end()); + }); + + //////////////////////////////////////////////////////////////////// + // 2. Element access — ALL return by copy (GC safety) + //////////////////////////////////////////////////////////////////// + + // at (triggers partial enumeration) + type.method("at", [](FP& self, size_t i) -> E { return self.at(i); }); + + // sorted_at (triggers full enumeration) + type.method("sorted_at", + [](FP& self, size_t i) -> E { return self.sorted_at(i); }); + + // sorted_at_no_checks + type.method("sorted_at_no_checks", [](FP& self, size_t i) -> E { + return self.sorted_at_no_checks(i); + }); + + // generator (const self) + type.method("generator", [](FP const& self, size_t i) -> E { + return self.generator(i); + }); + + // generator_no_checks (const self) + type.method("generator_no_checks", [](FP const& self, size_t i) -> E { + return self.generator_no_checks(i); + }); + + // getindex_no_checks — binds operator[], const self + type.method("getindex_no_checks", + [](FP const& self, size_t i) -> E { return self[i]; }); + + //////////////////////////////////////////////////////////////////// + // 3. Containment / position + //////////////////////////////////////////////////////////////////// + + // contains(FP&, E const&) -> bool + type.method("contains", [](FP& self, E const& x) -> bool { + return self.contains(x); + }); + + // position(FP&, E const&) -> element_index_type + type.method("position", [](FP& self, E const& x) -> uint32_t { + return self.position(x); + }); + + // current_position(FP const&, E const&) -> element_index_type + type.method("current_position", + [](FP const& self, E const& x) -> uint32_t { + return self.current_position(x); + }); + + // sorted_position(FP&, E const&) -> element_index_type + type.method("sorted_position", [](FP& self, E const& x) -> uint32_t { + return self.sorted_position(x); + }); + + // to_sorted_position(FP&, size_t) -> element_index_type + type.method("to_sorted_position", [](FP& self, size_t i) -> uint32_t { + return self.to_sorted_position(i); + }); + + //////////////////////////////////////////////////////////////////// + // 4. Fast product + //////////////////////////////////////////////////////////////////// + + type.method("fast_product", + [](FP const& self, size_t i, size_t j) -> uint32_t { + return self.fast_product(i, j); + }); + + type.method("fast_product_no_checks", + [](FP const& self, size_t i, size_t j) -> uint32_t { + return self.fast_product_no_checks(i, j); + }); + + //////////////////////////////////////////////////////////////////// + // 5. Idempotents + //////////////////////////////////////////////////////////////////// + + type.method("number_of_idempotents", [](FP& self) -> size_t { + return self.number_of_idempotents(); + }); + + type.method("is_idempotent", [](FP& self, size_t i) -> bool { + return self.is_idempotent(i); + }); + + type.method("is_idempotent_no_checks", [](FP& self, size_t i) -> bool { + return self.is_idempotent_no_checks(i); + }); + + //////////////////////////////////////////////////////////////////// + // 6. Modification + //////////////////////////////////////////////////////////////////// + + // add_generator! (void — Julia wrapper returns self) + type.method("add_generator!", + [](FP& self, E const& x) { self.add_generator(x); }); + + // add_generator_no_checks! + type.method("add_generator_no_checks!", [](FP& self, E const& x) { + self.add_generator_no_checks(x); + }); + + // closure! — single element wrapped in 1-element vector + type.method("closure!", [](FP& self, E const& x) { + std::vector v{x}; + self.closure(v.begin(), v.end()); + }); + + // copy_closure — single element, returns new FP by value + // NOTE: copy_closure is not const in C++ (it may enumerate) + type.method("copy_closure", [](FP& self, E const& x) -> FP { + std::vector v{x}; + return self.copy_closure(v.begin(), v.end()); + }); + + // copy_add_generators — single element, returns new FP by value + type.method("copy_add_generators", [](FP const& self, E const& x) -> FP { + std::vector v{x}; + return self.copy_add_generators(v.begin(), v.end()); + }); + + //////////////////////////////////////////////////////////////////// + // 7. Word-element conversion (ArrayRef for Julia Vector{UInt}) + //////////////////////////////////////////////////////////////////// + + // to_element — returns by copy (volatile const_reference!) + m.method("to_element", + [](FP const& self, jlcxx::ArrayRef arr) -> E { + word_type w(arr.begin(), arr.end()); + return self.to_element(w.begin(), w.end()); + }); + + // to_element_no_checks + m.method("to_element_no_checks", + [](FP const& self, jlcxx::ArrayRef arr) -> E { + word_type w(arr.begin(), arr.end()); + return self.to_element_no_checks(w.begin(), w.end()); + }); + + // equal_to — two words + m.method("equal_to", + [](FP const& self, + jlcxx::ArrayRef arr1, + jlcxx::ArrayRef arr2) -> bool { + word_type w1(arr1.begin(), arr1.end()); + word_type w2(arr2.begin(), arr2.end()); + return self.equal_to( + w1.begin(), w1.end(), w2.begin(), w2.end()); + }); + + // equal_to_no_checks + m.method("equal_to_no_checks", + [](FP const& self, + jlcxx::ArrayRef arr1, + jlcxx::ArrayRef arr2) -> bool { + word_type w1(arr1.begin(), arr1.end()); + word_type w2(arr2.begin(), arr2.end()); + return self.equal_to_no_checks( + w1.begin(), w1.end(), w2.begin(), w2.end()); + }); + + //////////////////////////////////////////////////////////////////// + // 8. Materialized collections + //////////////////////////////////////////////////////////////////// + + // idempotents — iterate cbegin_idempotents..cend_idempotents + // Use !(it == end) to avoid C++20 ambiguous reversed operator!= + m.method("idempotents", [](FP& self) -> std::vector { + std::vector result; + auto it = self.cbegin_idempotents(); + auto end = self.cend_idempotents(); + for (; !(it == end); ++it) { + result.push_back(*it); + } + return result; + }); + + // sorted_elements — iterate cbegin_sorted..cend_sorted + // Use !(it == end) to avoid C++20 ambiguous reversed operator!= + m.method("sorted_elements", [](FP& self) -> std::vector { + std::vector result; + auto it = self.cbegin_sorted(); + auto end = self.cend_sorted(); + for (; !(it == end); ++it) { + result.push_back(*it); + } + return result; + }); + + //////////////////////////////////////////////////////////////////// + // 9. Memory + //////////////////////////////////////////////////////////////////// + + type.method("reserve!", [](FP& self, size_t val) { self.reserve(val); }); + + //////////////////////////////////////////////////////////////////// + // 10. Display + //////////////////////////////////////////////////////////////////// + + m.method("to_human_readable_repr", [](FP const& self) -> std::string { + return libsemigroups::to_human_readable_repr(self); + }); + } + + } // anonymous namespace + + //////////////////////////////////////////////////////////////////////// + // define_froidure_pin — register all 10 FroidurePin instantiations + //////////////////////////////////////////////////////////////////////// + + void define_froidure_pin(jl::Module& m) { + using namespace libsemigroups; + + // Transf types + bind_froidure_pin>(m, "FroidurePinTransf1"); + bind_froidure_pin>(m, "FroidurePinTransf2"); + bind_froidure_pin>(m, "FroidurePinTransf4"); + + // PPerm types + bind_froidure_pin>(m, "FroidurePinPPerm1"); + bind_froidure_pin>(m, "FroidurePinPPerm2"); + bind_froidure_pin>(m, "FroidurePinPPerm4"); + + // Perm types + bind_froidure_pin>(m, "FroidurePinPerm1"); + bind_froidure_pin>(m, "FroidurePinPerm2"); + bind_froidure_pin>(m, "FroidurePinPerm4"); + + // BMat8 + bind_froidure_pin(m, "FroidurePinBMat8"); + } + +} // namespace libsemigroups_julia diff --git a/deps/src/libsemigroups_julia.cpp b/deps/src/libsemigroups_julia.cpp index ba5d9d7..0cd8b6e 100644 --- a/deps/src/libsemigroups_julia.cpp +++ b/deps/src/libsemigroups_julia.cpp @@ -40,6 +40,8 @@ namespace libsemigroups_julia { define_order(mod); define_word_range(mod); define_word_graph(mod); + define_froidure_pin_base(mod); + define_froidure_pin(mod); define_presentation(mod); define_presentation_examples(mod); } diff --git a/deps/src/libsemigroups_julia.hpp b/deps/src/libsemigroups_julia.hpp index b582894..3af5852 100644 --- a/deps/src/libsemigroups_julia.hpp +++ b/deps/src/libsemigroups_julia.hpp @@ -62,6 +62,8 @@ namespace libsemigroups_julia { void define_order(jl::Module& mod); void define_word_range(jl::Module& mod); void define_word_graph(jl::Module& mod); + void define_froidure_pin_base(jl::Module& mod); + void define_froidure_pin(jl::Module& mod); void define_presentation(jl::Module& mod); void define_presentation_examples(jl::Module& mod); diff --git a/docs/make.jl b/docs/make.jl index d20887f..f0def1e 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -66,6 +66,11 @@ makedocs(; "main-algorithms/core-classes/index.md", "main-algorithms/core-classes/runner.md", ], + "Froidure-Pin" => [ + "Overview" => "main-algorithms/froidure-pin/index.md", + "The FroidurePin type" => "main-algorithms/froidure-pin/froidure-pin.md", + "Helper functions" => "main-algorithms/froidure-pin/helpers.md", + ], ], ], warnonly = [:missing_docs, :linkcheck, :cross_references], diff --git a/docs/src/main-algorithms/froidure-pin/froidure-pin.md b/docs/src/main-algorithms/froidure-pin/froidure-pin.md new file mode 100644 index 0000000..0d5bdf4 --- /dev/null +++ b/docs/src/main-algorithms/froidure-pin/froidure-pin.md @@ -0,0 +1,204 @@ +# The FroidurePin type + +This page documents the parametric type +[`FroidurePin{E}`](@ref Semigroups.FroidurePin), which implements the +Froidure-Pin algorithm for enumerating the elements of a finitely +generated semigroup. + +`FroidurePin{E}` is a subtype of [`Runner`](@ref Semigroups.Runner) +(via the internal `FroidurePinBase`), so all runner methods +([`run!`](@ref), [`run_for!`](@ref), [`finished`](@ref), etc.) are +available. + +!!! warning "v1 limitation" + Semigroups.jl v1 binds `FroidurePin{E}` for the element types + [`Transf`](@ref Semigroups.Transf), + [`PPerm`](@ref Semigroups.PPerm), + [`Perm`](@ref Semigroups.Perm), and + [`BMat8`](@ref Semigroups.BMat8) only. + +## Table of contents + +| Section | Description | +| ------- | ----------- | +| [Construction](@ref) | Constructors from vectors or variadic generators. | +| [Size and enumeration](@ref) | Element count, degree, generator count, partial enumeration. | +| [Element access](@ref) | Access elements by index, generator index, or sorted index. | +| [Containment and position](@ref) | Membership testing and element position queries. | +| [Modification](@ref) | Adding generators, closure, copy-and-extend operations. | +| [Settings](@ref) | Batch size for partial enumeration. | +| [Predicates](@ref) | Identity containment, idempotent checks. | +| [Index queries](@ref) | Prefix/suffix, first/final letter, products, word lengths, rule counts. | +| [Iteration and display](@ref) | `for` loop iteration, `copy`, `show`. | +| [Runner interface](@ref) | Inherited `run!`, `run_for!`, `finished`, etc. | + +```@docs +Semigroups.FroidurePin +``` + +## Construction + +| Function | Description | +| -------- | ----------- | +| [`FroidurePin(gens::Vector{E})`](@ref Semigroups.FroidurePin(::Vector{E}) where E) | Construct from a vector of generators. | +| [`FroidurePin(x, xs...)`](@ref Semigroups.FroidurePin(::E, ::Vararg{E}) where E) | Construct from one or more generators (variadic). | + +```@docs +Semigroups.FroidurePin(::Vector{E}) where E +Semigroups.FroidurePin(::E, ::Vararg{E}) where E +``` + +## Size and enumeration + +| Function | Description | +| -------- | ----------- | +| [`length`](@ref Base.length(::FroidurePin)) | Total number of elements (triggers full enumeration). | +| [`current_size`](@ref Semigroups.current_size(::FroidurePin)) | Number of elements enumerated so far. | +| [`degree`](@ref Semigroups.degree(::FroidurePin)) | Degree of the elements. | +| [`number_of_generators`](@ref Semigroups.number_of_generators(::FroidurePin)) | Number of generators. | +| [`enumerate!`](@ref Semigroups.enumerate!(::FroidurePin, ::Integer)) | Enumerate until at least a given number of elements are found. | + +```@docs +Base.length(::FroidurePin) +Semigroups.current_size(::FroidurePin) +Semigroups.degree(::FroidurePin) +Semigroups.number_of_generators(::FroidurePin) +Semigroups.enumerate!(::FroidurePin, ::Integer) +``` + +## Element access + +| Function | Description | +| -------- | ----------- | +| [`getindex`](@ref Base.getindex(::FroidurePin{E}, ::Integer) where E) | Access element by 1-based index (triggers enumeration). | +| [`generator`](@ref Semigroups.generator(::FroidurePin{E}, ::Integer) where E) | Return the `i`-th generator. | +| [`sorted_at`](@ref Semigroups.sorted_at(::FroidurePin{E}, ::Integer) where E) | Access element by sorted index. | + +```@docs +Base.getindex(::FroidurePin{E}, ::Integer) where E +Semigroups.generator(::FroidurePin{E}, ::Integer) where E +Semigroups.sorted_at(::FroidurePin{E}, ::Integer) where E +``` + +## Containment and position + +| Function | Description | +| -------- | ----------- | +| [`in`](@ref Base.in(::E, ::FroidurePin{E}) where E) | Test membership of an element. | +| [`position`](@ref Semigroups.position(::FroidurePin{E}, ::E) where E) | Position of an element (triggers full enumeration). | +| [`sorted_position`](@ref Semigroups.sorted_position(::FroidurePin{E}, ::E) where E) | Sorted position of an element. | +| [`to_sorted_position`](@ref Semigroups.to_sorted_position(::FroidurePin, ::Integer)) | Convert element index to sorted index. | +| [`current_position`](@ref Semigroups.current_position(::FroidurePin{E}, ::E) where E) | Position among elements enumerated so far. | +| [`current_position`](@ref Semigroups.current_position(::FroidurePin, ::AbstractVector{<:Integer})) | Position of a word among elements enumerated so far. | + +```@docs +Base.in(::E, ::FroidurePin{E}) where E +Semigroups.position(::FroidurePin{E}, ::E) where E +Semigroups.sorted_position(::FroidurePin{E}, ::E) where E +Semigroups.to_sorted_position(::FroidurePin, ::Integer) +Semigroups.current_position(::FroidurePin{E}, ::E) where E +Semigroups.current_position(::FroidurePin, ::AbstractVector{<:Integer}) +``` + +## Modification + +| Function | Description | +| -------- | ----------- | +| [`push!`](@ref Base.push!(::FroidurePin{E}, ::E) where E) | Add a new generator. | +| [`closure!`](@ref Semigroups.closure!(::FroidurePin{E}, ::E) where E) | Add a non-redundant generator and re-enumerate. | +| [`copy_closure`](@ref Semigroups.copy_closure(::FroidurePin{E}, ::E) where E) | Copy and add a non-redundant generator. | +| [`copy_add_generators`](@ref Semigroups.copy_add_generators(::FroidurePin{E}, ::E) where E) | Copy and add a generator. | +| [`reserve!`](@ref Semigroups.reserve!(::FroidurePin, ::Integer)) | Pre-allocate storage for elements. | + +```@docs +Base.push!(::FroidurePin{E}, ::E) where E +Semigroups.closure!(::FroidurePin{E}, ::E) where E +Semigroups.copy_closure(::FroidurePin{E}, ::E) where E +Semigroups.copy_add_generators(::FroidurePin{E}, ::E) where E +Semigroups.reserve!(::FroidurePin, ::Integer) +``` + +## Settings + +| Function | Description | +| -------- | ----------- | +| [`batch_size`](@ref Semigroups.batch_size(::FroidurePin)) | Return the current batch size. | +| [`set_batch_size!`](@ref Semigroups.set_batch_size!(::FroidurePin, ::Integer)) | Set the batch size for partial enumeration. | + +```@docs +Semigroups.batch_size(::FroidurePin) +Semigroups.set_batch_size!(::FroidurePin, ::Integer) +``` + +## Predicates + +| Function | Description | +| -------- | ----------- | +| [`contains_one`](@ref Semigroups.contains_one(::FroidurePin)) | Check if the identity is an element. | +| [`currently_contains_one`](@ref Semigroups.currently_contains_one(::FroidurePin)) | Check if the identity is known to be an element so far. | +| [`is_idempotent`](@ref Semigroups.is_idempotent(::FroidurePin, ::Integer)) | Check if an element is an idempotent via its index. | + +```@docs +Semigroups.contains_one(::FroidurePin) +Semigroups.currently_contains_one(::FroidurePin) +Semigroups.is_idempotent(::FroidurePin, ::Integer) +``` + +## Index queries + +| Function | Description | +| -------- | ----------- | +| [`prefix`](@ref Semigroups.prefix(::FroidurePin, ::Integer)) | Position of the longest proper prefix. | +| [`suffix`](@ref Semigroups.suffix(::FroidurePin, ::Integer)) | Position of the longest proper suffix. | +| [`first_letter`](@ref Semigroups.first_letter(::FroidurePin, ::Integer)) | Index of the first generator in the factorisation. | +| [`final_letter`](@ref Semigroups.final_letter(::FroidurePin, ::Integer)) | Index of the last generator in the factorisation. | +| [`fast_product`](@ref Semigroups.fast_product(::FroidurePin, ::Integer, ::Integer)) | Position of the product of two elements by index. | +| [`product_by_reduction`](@ref Semigroups.product_by_reduction(::FroidurePin, ::Integer, ::Integer)) | Product using the Cayley graph. | +| [`position_of_generator`](@ref Semigroups.position_of_generator(::FroidurePin, ::Integer)) | Position of the `i`-th generator in the enumerated elements. | +| [`current_length`](@ref Semigroups.current_length(::FroidurePin, ::Integer)) | Length of the minimal factorisation (no enumeration). | +| [`word_length`](@ref Semigroups.word_length(::FroidurePin, ::Integer)) | Length of the minimal factorisation (with enumeration). | +| [`number_of_rules`](@ref Semigroups.number_of_rules(::FroidurePin)) | Total number of rules. | +| [`current_number_of_rules`](@ref Semigroups.current_number_of_rules(::FroidurePin)) | Number of rules found so far. | +| [`number_of_idempotents`](@ref Semigroups.number_of_idempotents(::FroidurePin)) | Total number of idempotent elements. | +| [`current_max_word_length`](@ref Semigroups.current_max_word_length(::FroidurePin)) | Maximum word length among elements enumerated so far. | +| [`number_of_elements_of_length`](@ref Semigroups.number_of_elements_of_length(::FroidurePin, ::Integer)) | Number of elements with a given factorisation length. | + +```@docs +Semigroups.prefix(::FroidurePin, ::Integer) +Semigroups.suffix(::FroidurePin, ::Integer) +Semigroups.first_letter(::FroidurePin, ::Integer) +Semigroups.final_letter(::FroidurePin, ::Integer) +Semigroups.fast_product(::FroidurePin, ::Integer, ::Integer) +Semigroups.product_by_reduction(::FroidurePin, ::Integer, ::Integer) +Semigroups.position_of_generator(::FroidurePin, ::Integer) +Semigroups.current_length(::FroidurePin, ::Integer) +Semigroups.word_length(::FroidurePin, ::Integer) +Semigroups.number_of_rules(::FroidurePin) +Semigroups.current_number_of_rules(::FroidurePin) +Semigroups.number_of_idempotents(::FroidurePin) +Semigroups.current_max_word_length(::FroidurePin) +Semigroups.number_of_elements_of_length(::FroidurePin, ::Integer) +``` + +## Iteration and display + +| Function | Description | +| -------- | ----------- | +| [`iterate`](@ref Base.iterate(::FroidurePin, ::Int)) | Iterate over all elements. | +| [`eltype`](@ref Base.eltype(::Type{FroidurePin{E}}) where E) | Return the element type `E`. | +| [`copy`](@ref Base.copy(::FroidurePin{E}) where E) | Create an independent copy. | +| [`show`](@ref Base.show(::IO, ::FroidurePin)) | Display a human-readable representation. | + +```@docs +Base.iterate(::FroidurePin, ::Int) +Base.eltype(::Type{FroidurePin{E}}) where E +Base.copy(::FroidurePin{E}) where E +Base.show(::IO, ::FroidurePin) +``` + +## Runner interface + +`FroidurePin{E}` inherits the full [`Runner`](@ref Semigroups.Runner) +interface. See the [Runner documentation](../core-classes/runner.md) for +the complete list of methods (`run!`, `run_for!`, `run_until!`, +`finished`, `started`, `timed_out`, `dead`, etc.). diff --git a/docs/src/main-algorithms/froidure-pin/helpers.md b/docs/src/main-algorithms/froidure-pin/helpers.md new file mode 100644 index 0000000..47354d9 --- /dev/null +++ b/docs/src/main-algorithms/froidure-pin/helpers.md @@ -0,0 +1,110 @@ +# Helper functions + +This page collects the free functions that operate on a +[`FroidurePin`](@ref Semigroups.FroidurePin) instance. They mirror the +`libsemigroups::froidure_pin::*` namespace and are organised into four +groups: factorisations, collections, word-element conversion, and Cayley +graphs. + +## Table of contents + +| Section | Description | +| ------- | ----------- | +| [Factorisations](@ref) | Minimal and non-minimal factorisations of elements as generator-index words. | +| [Collections](@ref) | Materialized vectors of rules, normal forms, idempotents, and sorted elements. | +| [Word-element conversion](@ref) | Convert between generator-index words and semigroup elements. | +| [Cayley graphs](@ref) | Left and right Cayley graphs as `WordGraph` objects. | + +## Factorisations + +These functions return or query words (as `Vector{Int}` with 1-based +generator indices) representing elements of the semigroup. + +### Contents + +| Function | Description | +| -------- | ----------- | +| [`minimal_factorisation`](@ref Semigroups.minimal_factorisation(::FroidurePin, ::Integer)) | Minimal factorisation of the element at a given position. | +| [`current_minimal_factorisation`](@ref Semigroups.current_minimal_factorisation(::FroidurePin, ::Integer)) | Minimal factorisation without triggering enumeration. | +| [`factorisation`](@ref Semigroups.factorisation(::FroidurePin, ::Integer)) | A (not necessarily minimal) factorisation. | + +### Full API + +```@docs +Semigroups.minimal_factorisation(::FroidurePin, ::Integer) +Semigroups.current_minimal_factorisation(::FroidurePin, ::Integer) +Semigroups.factorisation(::FroidurePin, ::Integer) +``` + +## Collections + +These functions return materialized collections of elements, rules, or +normal forms. + +### Contents + +| Function | Description | +| -------- | ----------- | +| [`rules`](@ref Semigroups.rules(::FroidurePin)) | All rules as `lhs => rhs` pairs with 1-based generator indices. | +| [`current_rules`](@ref Semigroups.current_rules(::FroidurePin)) | Rules discovered so far. | +| [`normal_forms`](@ref Semigroups.normal_forms(::FroidurePin)) | Normal forms for all elements. | +| [`current_normal_forms`](@ref Semigroups.current_normal_forms(::FroidurePin)) | Normal forms discovered so far. | +| [`idempotents`](@ref Semigroups.idempotents(::FroidurePin{E}) where E) | All idempotent elements. | +| [`sorted_elements`](@ref Semigroups.sorted_elements(::FroidurePin{E}) where E) | All elements in sorted order. | + +### Full API + +```@docs +Semigroups.rules(::FroidurePin) +Semigroups.current_rules(::FroidurePin) +Semigroups.normal_forms(::FroidurePin) +Semigroups.current_normal_forms(::FroidurePin) +Semigroups.idempotents(::FroidurePin{E}) where E +Semigroups.sorted_elements(::FroidurePin{E}) where E +``` + +## Word-element conversion + +These functions convert between generator-index words (`Vector{Int}` +with 1-based indices) and semigroup elements. + +### Contents + +| Function | Description | +| -------- | ----------- | +| [`to_element`](@ref Semigroups.to_element(::FroidurePin{E}, ::AbstractVector{<:Integer}) where E) | Convert a word to the corresponding element. | +| [`equal_to`](@ref Semigroups.equal_to(::FroidurePin, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})) | Check if two words represent the same element. | +| [`position`](@ref Semigroups.position(::FroidurePin, ::AbstractVector{<:Integer})) | Position of the element represented by a word. | + +### Full API + +```@docs +Semigroups.to_element(::FroidurePin{E}, ::AbstractVector{<:Integer}) where E +Semigroups.equal_to(::FroidurePin, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer}) +Semigroups.position(::FroidurePin, ::AbstractVector{<:Integer}) +``` + +## Cayley graphs + +The left and right Cayley graphs of a `FroidurePin` instance are +returned as [`WordGraph`](@ref Semigroups.WordGraph) objects. The +`current_*` variants return the graph for elements enumerated so far +without triggering further enumeration. + +### Contents + +| Function | Description | +| -------- | ----------- | +| [`right_cayley_graph`](@ref Semigroups.right_cayley_graph(::FroidurePin)) | Right Cayley graph (triggers full enumeration). | +| [`current_right_cayley_graph`](@ref Semigroups.current_right_cayley_graph(::FroidurePin)) | Right Cayley graph for elements enumerated so far. | +| [`left_cayley_graph`](@ref Semigroups.left_cayley_graph(::FroidurePin)) | Left Cayley graph (triggers full enumeration). | +| [`current_left_cayley_graph`](@ref Semigroups.current_left_cayley_graph(::FroidurePin)) | Left Cayley graph for elements enumerated so far. | + +### Full API + +```@docs +Semigroups.right_cayley_graph(::FroidurePin) +Semigroups.current_right_cayley_graph(::FroidurePin) +Semigroups.left_cayley_graph(::FroidurePin) +Semigroups.current_left_cayley_graph(::FroidurePin) +``` diff --git a/docs/src/main-algorithms/froidure-pin/index.md b/docs/src/main-algorithms/froidure-pin/index.md new file mode 100644 index 0000000..36bd056 --- /dev/null +++ b/docs/src/main-algorithms/froidure-pin/index.md @@ -0,0 +1,22 @@ +# Froidure-Pin + +This section contains documentation related to the implementation of the +Froidure-Pin algorithm in Semigroups.jl. + +The purpose of the Froidure-Pin algorithm is to determine the elements +and structure of a semigroup or monoid generated by a set of generators +whose multiplication and comparison for equality is decidable (such as +matrices, transformations, and so on). + +!!! warning "v1 limitation" + Semigroups.jl v1 binds `FroidurePin{E}` for the following element + types only: [`Transf`](@ref Semigroups.Transf), + [`PPerm`](@ref Semigroups.PPerm), [`Perm`](@ref Semigroups.Perm), + and [`BMat8`](@ref Semigroups.BMat8). + +## Contents + +| Page | Description | +| -------------------------------------------------- | ------------------------------------------------------------------------------------- | +| [The FroidurePin type](froidure-pin.md) | The main [`FroidurePin{E}`](@ref Semigroups.FroidurePin) parametric type. | +| [Helper functions](helpers.md) | Free functions for factorisations, word queries, collections, and Cayley graphs. | diff --git a/docs/src/main-algorithms/index.md b/docs/src/main-algorithms/index.md index b7f41c9..317b30d 100644 --- a/docs/src/main-algorithms/index.md +++ b/docs/src/main-algorithms/index.md @@ -1,19 +1,21 @@ # Overview -This section will document the main algorithms available in Semigroups.jl. +This section documents the main algorithms available in Semigroups.jl. -## Planned Algorithms - -The following algorithms from libsemigroups will be exposed through Semigroups.jl: +## Available Algorithms -### Froidure-Pin Algorithm +### [Froidure-Pin Algorithm](froidure-pin/index.md) -### Knuth-Bendix Completion +The Froidure-Pin algorithm enumerates all elements and determines the +structure of a finitely generated semigroup or monoid. See the +[Froidure-Pin section](froidure-pin/index.md) for the full API. -### Todd-Coxeter Algorithm - -### Schreier-Sims Algorithm +## Planned Algorithms -## Status +The following algorithms from libsemigroups will be exposed in future +versions of Semigroups.jl: -This section is under development. +- **Knuth-Bendix Completion** +- **Todd-Coxeter Algorithm** +- **Kambites' Algorithm** (small overlap monoids) +- **Schreier-Sims Algorithm** (permutation groups) diff --git a/src/Semigroups.jl b/src/Semigroups.jl index f69ebba..f8f88e1 100644 --- a/src/Semigroups.jl +++ b/src/Semigroups.jl @@ -8,6 +8,7 @@ module Semigroups using CxxWrap using AbstractAlgebra +import Dates using Dates: TimePeriod, Nanosecond using libsemigroups_jll @@ -74,6 +75,9 @@ include("presentation-examples.jl") include("bmat8.jl") include("transf.jl") +# Algorithm types (must come after element types) +include("froidure-pin.jl") + # Module initialization function __init__() # Initialize the CxxWrap module @@ -160,6 +164,26 @@ export degree, rank, image, domain, inverse export increase_degree_by!, swap! export left_one, right_one +# FroidurePin +export FroidurePin, current_size, number_of_generators, enumerate! +export generator, sorted_at +export sorted_position, to_sorted_position +export closure!, copy_closure, copy_add_generators, reserve! +export batch_size, set_batch_size! +export current_position +export contains_one, currently_contains_one, is_idempotent +export prefix, suffix, first_letter, final_letter, fast_product +export number_of_idempotents, number_of_elements_of_length +export current_number_of_rules, current_max_word_length +export position_of_generator, current_length, word_length +export product_by_reduction +export rules, current_rules, normal_forms, current_normal_forms +export idempotents, sorted_elements +export minimal_factorisation, current_minimal_factorisation, factorisation +export right_cayley_graph, current_right_cayley_graph +export left_cayley_graph, current_left_cayley_graph +export to_element, equal_to + # BMat8 export BMat8, to_int, swap!, degree, random, row_space_basis export col_space_basis, col_space_size, is_regular_element, minimum_dim diff --git a/src/froidure-pin.jl b/src/froidure-pin.jl new file mode 100644 index 0000000..983dc79 --- /dev/null +++ b/src/froidure-pin.jl @@ -0,0 +1,1921 @@ +# Copyright (c) 2026, James W. Swent, J. D. Mitchell +# +# Distributed under the terms of the GPL license version 3. +# +# The full license is in the file LICENSE, distributed with this software. + +""" +froidure-pin.jl - FroidurePin{E} high-level Julia wrapper + +Provides a parametric `FroidurePin{E}` type wrapping the CxxWrap-bound +C++ `FroidurePin` template instantiations. Follows the same three-layer +pattern as `transf.jl`: CxxWrap type aliases, union type, private helpers, +mutable struct, constructors, and method wrappers. +""" + +# ============================================================================ +# CxxWrap type aliases — concrete FroidurePin instantiations +# ============================================================================ + +const FroidurePinBase = LibSemigroups.FroidurePinBase + +const FroidurePinTransf1 = LibSemigroups.FroidurePinTransf1 +const FroidurePinTransf2 = LibSemigroups.FroidurePinTransf2 +const FroidurePinTransf4 = LibSemigroups.FroidurePinTransf4 + +const FroidurePinPPerm1 = LibSemigroups.FroidurePinPPerm1 +const FroidurePinPPerm2 = LibSemigroups.FroidurePinPPerm2 +const FroidurePinPPerm4 = LibSemigroups.FroidurePinPPerm4 + +const FroidurePinPerm1 = LibSemigroups.FroidurePinPerm1 +const FroidurePinPerm2 = LibSemigroups.FroidurePinPerm2 +const FroidurePinPerm4 = LibSemigroups.FroidurePinPerm4 + +const FroidurePinBMat8 = LibSemigroups.FroidurePinBMat8 + +# ============================================================================ +# Union type for cxx_obj field +# ============================================================================ + +const _FroidurePinCxx = Union{ + FroidurePinTransf1, + FroidurePinTransf2, + FroidurePinTransf4, + FroidurePinPPerm1, + FroidurePinPPerm2, + FroidurePinPPerm4, + FroidurePinPerm1, + FroidurePinPerm2, + FroidurePinPerm4, + FroidurePinBMat8, +} + +# ============================================================================ +# Private helpers +# ============================================================================ +# Index/word conversion helpers (_to_cpp, _from_cpp, _word_to_cpp, +# _word_from_cpp) are defined in word-graph.jl, transf.jl, and order.jl +# respectively. We reuse them here without redefinition. + +# ============================================================================ +# Type dispatch helpers +# ============================================================================ + +""" + _cxx_fp_type(::Type{E}) -> Type + +Map a high-level Julia element type to its CxxWrap FroidurePin constructor type. +""" +_cxx_fp_type(::Type{Transf{UInt8}}) = FroidurePinTransf1 +_cxx_fp_type(::Type{Transf{UInt16}}) = FroidurePinTransf2 +_cxx_fp_type(::Type{Transf{UInt32}}) = FroidurePinTransf4 + +_cxx_fp_type(::Type{PPerm{UInt8}}) = FroidurePinPPerm1 +_cxx_fp_type(::Type{PPerm{UInt16}}) = FroidurePinPPerm2 +_cxx_fp_type(::Type{PPerm{UInt32}}) = FroidurePinPPerm4 + +_cxx_fp_type(::Type{Perm{UInt8}}) = FroidurePinPerm1 +_cxx_fp_type(::Type{Perm{UInt16}}) = FroidurePinPerm2 +_cxx_fp_type(::Type{Perm{UInt32}}) = FroidurePinPerm4 + +_cxx_fp_type(::Type{T}) where {T<:BMat8} = FroidurePinBMat8 + +""" + _wrap_element(::Type{E}, raw) -> E + +Wrap a raw CxxWrap element back into the high-level Julia type. +""" +_wrap_element(::Type{Transf{T}}, raw) where {T} = Transf{T}(raw) +_wrap_element(::Type{PPerm{T}}, raw) where {T} = PPerm{T}(raw) +_wrap_element(::Type{Perm{T}}, raw) where {T} = Perm{T}(raw) +_wrap_element(::Type{T}, raw) where {T<:BMat8} = raw # BMat8 is a direct alias + +""" + _cxx_element(x) -> CxxWrap object + +Extract the CxxWrap object from a high-level Julia element. +""" +_cxx_element(x::Transf) = x.cxx_obj +_cxx_element(x::PPerm) = x.cxx_obj +_cxx_element(x::Perm) = x.cxx_obj +_cxx_element(x::BMat8) = x # BMat8 is a direct alias + +""" + _copy_cxx_element(raw) -> CxxWrap object (Allocated) + +Copy a raw CxxWrap element to produce an Allocated (owning) copy. +Needed when iterating CxxWrap StdVectors, which yield Dereferenced +references that don't own their memory. + +For Transf/PPerm/Perm types, calls LibSemigroups.copy(). +For BMat8, returns as-is (value type). +""" +_copy_cxx_element(raw::Union{_TransfTypes,_PPermTypes,_PermTypes}) = LibSemigroups.copy(raw) +_copy_cxx_element(raw::BMat8) = raw + +""" + _fp_element_type(::Type{E}) -> Type + +Normalize element type for the FroidurePin{E} type parameter. +CxxWrap creates BMat8Allocated as a subtype of BMat8; we normalize +to BMat8 so that FroidurePin{BMat8} is the canonical type. +""" +_fp_element_type(::Type{E}) where {E} = E +_fp_element_type(::Type{T}) where {T<:BMat8} = BMat8 + +# ============================================================================ +# FroidurePin{E} mutable struct +# ============================================================================ + +""" + FroidurePin{E} + +Type implementing the Froidure-Pin algorithm. + +A `FroidurePin{E}` instance is defined by a generating set of elements of +type `E`, and the main function is [`run!`](@ref), which implements the +Froidure-Pin algorithm as described by Froidure and Pin. If [`run!`](@ref) +is invoked and [`finished`](@ref) returns `true`, then the size +[`length`](@ref), the left and right Cayley graphs +[`left_cayley_graph`](@ref) and [`right_cayley_graph`](@ref) are +determined, and a confluent terminating presentation for the semigroup is +known. + +Supported element types: +- [`Transf{UInt8}`](@ref Semigroups.Transf), [`Transf{UInt16}`](@ref Semigroups.Transf), [`Transf{UInt32}`](@ref Semigroups.Transf) (transformations) +- [`PPerm{UInt8}`](@ref Semigroups.PPerm), [`PPerm{UInt16}`](@ref Semigroups.PPerm), [`PPerm{UInt32}`](@ref Semigroups.PPerm) (partial permutations) +- [`Perm{UInt8}`](@ref Semigroups.Perm), [`Perm{UInt16}`](@ref Semigroups.Perm), [`Perm{UInt32}`](@ref Semigroups.Perm) (permutations) +- [`BMat8`](@ref Semigroups.BMat8) (boolean matrices up to 8x8) + +# Example +```julia +using Semigroups + +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +length(S) # 6 (symmetric group S_3) +``` + +# See also +- [`FroidurePinBase`](@ref Semigroups.FroidurePinBase) +- [`Runner`](@ref Semigroups.Runner) +""" +mutable struct FroidurePin{E} + cxx_obj::_FroidurePinCxx +end + +# ============================================================================ +# Constructors +# ============================================================================ + +""" + FroidurePin(gens::Vector{E}) where {E} + +Construct a [`FroidurePin{E}`](@ref Semigroups.FroidurePin) from a +container of generators. + +This function constructs a `FroidurePin{E}` instance from the vector of +generators `gens`, after verifying that the proposed generators all have +equal degree. + +# Arguments +- `gens::Vector{E}`: a non-empty vector of generators. All generators + must have the same degree. + +# Throws +- `ErrorException`: if `gens` is empty. +- `LibsemigroupsError`: if `degree(x) != degree(y)` for any `x` and `y` + in `gens`. + +# Example +```julia +using Semigroups + +S = FroidurePin([Transf([2, 1, 3]), Transf([2, 3, 1])]) +length(S) # 6 +``` + +# See also +- [`FroidurePin(x::E, xs::E...)`](@ref) +""" +function FroidurePin(gens::Vector{E}) where {E} + isempty(gens) && error("At least one generator is required") + + # Normalize element type (BMat8Allocated -> BMat8) + NE = _fp_element_type(E) + + FPType = _cxx_fp_type(NE) + cxx_gens = [_cxx_element(g) for g in gens] + + n = length(cxx_gens) + if n == 1 + cxx_obj = @wrap_libsemigroups_call FPType(cxx_gens[1]) + elseif n == 2 + cxx_obj = @wrap_libsemigroups_call FPType(cxx_gens[1], cxx_gens[2]) + elseif n == 3 + cxx_obj = @wrap_libsemigroups_call FPType(cxx_gens[1], cxx_gens[2], cxx_gens[3]) + elseif n == 4 + cxx_obj = @wrap_libsemigroups_call FPType( + cxx_gens[1], + cxx_gens[2], + cxx_gens[3], + cxx_gens[4], + ) + else + # >4 generators: construct with first, then add the rest + cxx_obj = @wrap_libsemigroups_call FPType(cxx_gens[1]) + for i = 2:n + @wrap_libsemigroups_call LibSemigroups.add_generator!(cxx_obj, cxx_gens[i]) + end + end + + return FroidurePin{NE}(cxx_obj) +end + +""" + FroidurePin(x::E, xs::E...) where {E} + +Construct a [`FroidurePin{E}`](@ref Semigroups.FroidurePin) from one or +more generators given as positional arguments. + +This is a convenience constructor equivalent to +`FroidurePin(E[x, xs...])`. + +# Arguments +- `x::E`: the first generator. +- `xs::E...`: zero or more additional generators of the same type. + +# Throws +- `LibsemigroupsError`: if `degree(x) != degree(y)` for any generators + `x` and `y`. + +# Example +```julia +using Semigroups + +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +length(S) # 6 +``` + +# See also +- [`FroidurePin(gens::Vector{E})`](@ref) +""" +function FroidurePin(x::E, xs::E...) where {E} + return FroidurePin(E[x, xs...]) +end + +# ============================================================================ +# Size queries +# ============================================================================ + +""" + Base.length(fp::FroidurePin) -> Int + +Return the size of a [`FroidurePin`](@ref Semigroups.FroidurePin) instance. + +This function fully enumerates `fp` and then returns the number of +elements. + +# Complexity +At worst ``O(|S| n)`` where ``S`` is the semigroup represented by `fp` +and ``n`` is [`number_of_generators`](@ref)`(fp)`. + +!!! note + This function triggers a full enumeration. + +# Example +```julia +using Semigroups + +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +length(S) # 6 +``` + +# See also +- [`current_size`](@ref) +""" +Base.length(fp::FroidurePin) = Int(LibSemigroups.size(fp.cxx_obj)) + +""" + current_size(fp::FroidurePin) -> Int + +Return the number of elements so far enumerated. + +This is only the actual size of the semigroup if `fp` is fully enumerated. + +# Complexity +Constant. + +!!! note + This function does not trigger any enumeration. + +# Example +```julia +using Semigroups + +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +current_size(S) # 2 (only generators known) +``` + +# See also +- [`length`](@ref Base.length) +""" +current_size(fp::FroidurePin) = Int(LibSemigroups.current_size(fp.cxx_obj)) + +""" + degree(fp::FroidurePin) -> Int + +Return the degree of any and all elements. + +This function returns the degree of any (and hence all) elements of the +semigroup represented by `fp`. + +# Complexity +Constant. + +!!! note + This function does not trigger any enumeration. + +# Example +```julia +using Semigroups + +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +degree(S) # 3 +``` +""" +degree(fp::FroidurePin) = Int(LibSemigroups.degree(fp.cxx_obj)) + +""" + number_of_generators(fp::FroidurePin) -> Int + +Return the number of generators. + +This function returns the number of generators of the semigroup +represented by `fp`. + +!!! note + This function does not trigger any enumeration. + +# Example +```julia +using Semigroups + +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +number_of_generators(S) # 2 +``` + +# See also +- [`generator`](@ref) +""" +number_of_generators(fp::FroidurePin) = Int(LibSemigroups.number_of_generators(fp.cxx_obj)) + +""" + enumerate!(fp::FroidurePin, limit::Integer) -> FroidurePin + +Enumerate until at least a specified number of elements are found. + +If `fp` is already fully enumerated, or the number of elements +previously enumerated exceeds `limit`, then calling this function does +nothing. Otherwise, [`run!`](@ref) attempts to find at least the maximum +of `limit` and [`batch_size`](@ref) additional elements of the semigroup. + +# Arguments +- `fp::FroidurePin`: the FroidurePin instance. +- `limit::Integer`: the approximate limit for + [`current_size`](@ref)`(fp)`. + +# Complexity +At worst ``O(m n)`` where ``m`` equals `limit` and ``n`` is +[`number_of_generators`](@ref)`(fp)`. + +# Example +```julia +using Semigroups + +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +enumerate!(S, 4) +current_size(S) # >= 4 +``` + +# See also +- [`run!`](@ref) +- [`current_size`](@ref) +""" +function enumerate!(fp::FroidurePin, limit::Integer) + lim = UInt(limit) + @wrap_libsemigroups_call LibSemigroups.enumerate!(fp.cxx_obj, lim) + return fp +end + +# ============================================================================ +# Runner delegation +# ============================================================================ +# Every public Runner method is delegated through fp.cxx_obj. +# Mutating methods return fp for chaining; query methods return the result. + +""" + run!(fp::FroidurePin) -> FroidurePin + +Run the Froidure-Pin algorithm until [`finished`](@ref). + +At the end of this call `fp` is either [`finished`](@ref) or +[`dead`](@ref). Returns `fp` for method chaining. + +# See also +- [`run_for!`](@ref) +- [`run_until!`](@ref) +- [`finished`](@ref) +""" +function run!(fp::FroidurePin) + LibSemigroups.run!(fp.cxx_obj) + return fp +end + +""" + run_for!(fp::FroidurePin, t::TimePeriod) -> FroidurePin + +Run the Froidure-Pin algorithm for a specified amount of time. + +At the end of this call `fp` is either [`finished`](@ref), +[`dead`](@ref), or [`timed_out`](@ref). Returns `fp` for method +chaining. + +# Arguments +- `fp::FroidurePin`: the FroidurePin instance. +- `t::TimePeriod`: the duration to run for (e.g. `Second(1)`, + `Millisecond(500)`). + +# Example +```julia +run_for!(S, Second(1)) +run_for!(S, Millisecond(500)) +``` + +# See also +- [`run!`](@ref) +- [`run_until!`](@ref) +- [`timed_out`](@ref) +""" +function run_for!(fp::FroidurePin, t::TimePeriod) + ns = convert(Nanosecond, t) + Dates.value(ns) >= 0 || + throw(ArgumentError("run_for! requires a non-negative duration, got $t")) + LibSemigroups.run_for!(fp.cxx_obj, Int64(Dates.value(ns))) + return fp +end + +function run_until!(f::Function, fp::FroidurePin) + sf = @safe_cfunction($f, Cuchar, ()) + GC.@preserve sf LibSemigroups.run_until!(fp.cxx_obj, sf) + return fp +end + +""" + run_until!(fp::FroidurePin, f::Function) -> FroidurePin + +Run the Froidure-Pin algorithm until a nullary predicate returns `true` +or [`finished`](@ref). + +At the end of this call `fp` is either [`finished`](@ref), +[`dead`](@ref), or [`stopped_by_predicate`](@ref). Returns `fp` for +method chaining. + +# Arguments +- `fp::FroidurePin`: the FroidurePin instance. +- `f::Function`: a nullary predicate (a callable taking no arguments and + returning `Bool`). + +Supports do-block syntax: + +```julia +run_until!(S) do + current_size(S) >= 10 +end +``` + +# See also +- [`run!`](@ref) +- [`run_for!`](@ref) +- [`stopped_by_predicate`](@ref) +""" +run_until!(fp::FroidurePin, f::Function) = run_until!(f, fp) + +""" + init!(fp::FroidurePin) -> FroidurePin + +Initialize an existing [`FroidurePin`](@ref Semigroups.FroidurePin) +object. + +This function puts `fp` back into the same state as if it had been newly +default constructed. Returns `fp` for method chaining. + +!!! warning + This function is not thread-safe. + +# See also +- [`FroidurePin`](@ref Semigroups.FroidurePin) +""" +function init!(fp::FroidurePin) + LibSemigroups.init!(fp.cxx_obj) + return fp +end + +""" + kill!(fp::FroidurePin) + +Stop [`run!`](@ref) from running (thread-safe). + +This function can be used to terminate [`run!`](@ref) from another +thread. After [`kill!`](@ref) has been called, `fp` may no longer be in a +valid state, but will return `true` from [`dead`](@ref). + +# See also +- [`dead`](@ref) +- [`finished`](@ref) +""" +kill!(fp::FroidurePin) = LibSemigroups.kill!(fp.cxx_obj) + +""" + finished(fp::FroidurePin) -> Bool + +Check if [`run!`](@ref) has been run to completion. + +Returns `true` if [`run!`](@ref) has been run to completion and `false` +otherwise. + +# See also +- [`started`](@ref) +- [`stopped`](@ref) +""" +finished(fp::FroidurePin) = LibSemigroups.finished(fp.cxx_obj) + +""" + Base.success(fp::FroidurePin) -> Bool + +Check if [`run!`](@ref) has been run to completion successfully. + +Returns `true` if [`run!`](@ref) has been run to completion and it was +successful. The default implementation is equivalent to calling +[`finished`](@ref). + +# See also +- [`finished`](@ref) +""" +Base.success(fp::FroidurePin) = LibSemigroups.success(fp.cxx_obj) + +""" + started(fp::FroidurePin) -> Bool + +Check if [`run!`](@ref) has been called at least once. + +Returns `true` if [`run!`](@ref) has started to run (it can be +currently running or not). + +# See also +- [`finished`](@ref) +- [`running`](@ref) +""" +started(fp::FroidurePin) = LibSemigroups.started(fp.cxx_obj) + +""" + running(fp::FroidurePin) -> Bool + +Check if currently running. + +Returns `true` if [`run!`](@ref) is in the process of running and +`false` otherwise. + +# See also +- [`finished`](@ref) +- [`started`](@ref) +""" +running(fp::FroidurePin) = LibSemigroups.running(fp.cxx_obj) + +""" + timed_out(fp::FroidurePin) -> Bool + +Check if the amount of time passed to [`run_for!`](@ref) has elapsed. + +Returns `true` if the last [`run_for!`](@ref) call timed out and `false` +otherwise. + +# See also +- [`run_for!`](@ref) +- [`stopped`](@ref) +""" +timed_out(fp::FroidurePin) = LibSemigroups.timed_out(fp.cxx_obj) + +""" + stopped(fp::FroidurePin) -> Bool + +Check if the runner is stopped. + +This function can be used to check whether or not [`run!`](@ref) has +been stopped for whatever reason. In other words, it checks if +[`timed_out`](@ref), [`finished`](@ref), or [`dead`](@ref). + +# See also +- [`timed_out`](@ref) +- [`finished`](@ref) +- [`dead`](@ref) +""" +stopped(fp::FroidurePin) = LibSemigroups.stopped(fp.cxx_obj) + +""" + dead(fp::FroidurePin) -> Bool + +Check if the runner is dead. + +This function can be used to check if [`run!`](@ref) should terminate +because it has been killed by another thread via [`kill!`](@ref). + +# See also +- [`kill!`](@ref) +- [`stopped`](@ref) +""" +dead(fp::FroidurePin) = LibSemigroups.dead(fp.cxx_obj) + +""" + stopped_by_predicate(fp::FroidurePin) -> Bool + +Check if the runner was stopped by the predicate passed to +[`run_until!`](@ref). + +If `fp` is running, then the nullary predicate is called and its return +value is returned. If `fp` is not running, then `true` is returned if +and only if the last time `fp` was running it was stopped by the +predicate passed to [`run_until!`](@ref). + +# Complexity +Constant. + +# See also +- [`run_until!`](@ref) +- [`stopped`](@ref) +""" +stopped_by_predicate(fp::FroidurePin) = LibSemigroups.stopped_by_predicate(fp.cxx_obj) + +""" + running_for(fp::FroidurePin) -> Bool + +Check if currently running for a particular length of time. + +If `fp` is currently running because [`run_for!`](@ref) has been +invoked, then this function returns `true`. Otherwise, `false` is +returned. + +# Complexity +Constant. + +# See also +- [`run_for!`](@ref) +- [`running_for_how_long`](@ref) +""" +running_for(fp::FroidurePin) = LibSemigroups.running_for(fp.cxx_obj) + +""" + running_for_how_long(fp::FroidurePin) -> Nanosecond + +Return the last value passed to [`run_for!`](@ref). + +This function returns the last value passed as an argument to +[`run_for!`](@ref) (if any) as a `Dates.Nanosecond`. + +# Complexity +Constant. + +# See also +- [`run_for!`](@ref) +- [`running_for`](@ref) +""" +running_for_how_long(fp::FroidurePin) = + Nanosecond(LibSemigroups.running_for_how_long(fp.cxx_obj)) + +""" + running_until(fp::FroidurePin) -> Bool + +Check if currently running until a nullary predicate returns `true`. + +If `fp` is currently running because [`run_until!`](@ref) has been +invoked, then this function returns `true`. Otherwise, `false` is +returned. + +# Complexity +Constant. + +# See also +- [`run_until!`](@ref) +- [`stopped_by_predicate`](@ref) +""" +running_until(fp::FroidurePin) = LibSemigroups.running_until(fp.cxx_obj) + +""" + current_state(fp::FroidurePin) -> RunnerState + +Return the current state of the runner. + +Returns the current state of `fp` as a [`RunnerState`](@ref Semigroups.RunnerState) +value. + +# Complexity +Constant. + +# See also +- [`RunnerState`](@ref Semigroups.RunnerState) +""" +current_state(fp::FroidurePin) = LibSemigroups.current_state(fp.cxx_obj) + +""" + report_why_we_stopped(fp::FroidurePin) + +Report why [`run!`](@ref) stopped. + +Reports whether [`run!`](@ref) was stopped because it is +[`finished`](@ref), [`timed_out`](@ref), or [`dead`](@ref). + +# See also +- [`string_why_we_stopped`](@ref) +""" +report_why_we_stopped(fp::FroidurePin) = LibSemigroups.report_why_we_stopped(fp.cxx_obj) + +""" + string_why_we_stopped(fp::FroidurePin) -> String + +Return a human-readable string describing why [`run!`](@ref) stopped. + +Returns a string indicating whether [`run!`](@ref) was stopped because +it is [`finished`](@ref), [`timed_out`](@ref), or [`dead`](@ref). + +# See also +- [`report_why_we_stopped`](@ref) +""" +string_why_we_stopped(fp::FroidurePin) = LibSemigroups.string_why_we_stopped(fp.cxx_obj) + +# ============================================================================ +# Element access +# ============================================================================ + +""" + Base.getindex(fp::FroidurePin{E}, i::Integer) -> E + +Access the element with index `i`. + +This function attempts to enumerate until at least `i` elements have +been found, then returns the element at position `i`. + +# Arguments +- `fp::FroidurePin{E}`: the FroidurePin instance. +- `i::Integer`: the 1-based index of the element to access. + +# Throws +- `BoundsError`: if `i` is less than `1` or greater than + [`length`](@ref Base.length)`(fp)`. + +!!! note + This function triggers a full enumeration. + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +S[1] # first element +``` + +# See also +- [`sorted_at`](@ref) +- [`generator`](@ref) +""" +function Base.getindex(fp::FroidurePin{E}, i::Integer) where {E} + if i < 1 || i > length(fp) + throw(BoundsError(fp, i)) + end + idx = _to_cpp(i, UInt) + raw = @wrap_libsemigroups_call LibSemigroups.at(fp.cxx_obj, idx) + return _wrap_element(E, raw) +end + +""" + generator(fp::FroidurePin{E}, i::Integer) -> E + +Return the generator with the specified index. + +This function returns the generator with index `i`, where the order is +that in which the generators were added at construction, or via +[`push!`](@ref Base.push!), [`closure!`](@ref), +[`copy_closure`](@ref), or [`copy_add_generators`](@ref). + +# Arguments +- `fp::FroidurePin{E}`: the FroidurePin instance. +- `i::Integer`: the 1-based index of a generator. + +# Throws +- `LibsemigroupsError`: if `i` is greater than + [`number_of_generators`](@ref)`(fp)`. + +!!! note + `generator(fp, j)` is in general not in position `j`. + +!!! note + This function does not trigger any enumeration. + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +generator(S, 1) # first generator +``` + +# See also +- [`getindex`](@ref Base.getindex) +- [`sorted_at`](@ref) +""" +function generator(fp::FroidurePin{E}, i::Integer) where {E} + idx = _to_cpp(i, UInt) + raw = @wrap_libsemigroups_call LibSemigroups.generator(fp.cxx_obj, idx) + return _wrap_element(E, raw) +end + +""" + sorted_at(fp::FroidurePin{E}, i::Integer) -> E + +Access the element with the specified sorted index. + +This function triggers a full enumeration, and returns the element +at position `i` when the elements are sorted. + +# Arguments +- `fp::FroidurePin{E}`: the FroidurePin instance. +- `i::Integer`: the 1-based sorted index of the element to access. + +# Throws +- `LibsemigroupsError`: if `i` is greater than + [`length`](@ref Base.length)`(fp)`. + +!!! note + This function triggers a full enumeration. + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +sorted_at(S, 1) # first element in sorted order +``` + +# See also +- [`getindex`](@ref Base.getindex) +- [`sorted_position`](@ref) +""" +function sorted_at(fp::FroidurePin{E}, i::Integer) where {E} + idx = _to_cpp(i, UInt) + raw = @wrap_libsemigroups_call LibSemigroups.sorted_at(fp.cxx_obj, idx) + return _wrap_element(E, raw) +end + +# ============================================================================ +# Iteration +# ============================================================================ + +""" + Base.iterate(fp::FroidurePin{E}[, state]) -> Union{Tuple{E, Int}, Nothing} + +Iterate over all elements of the semigroup. + +This function allows a [`FroidurePin{E}`](@ref Semigroups.FroidurePin) +instance to be used in `for` loops and with `collect`. Elements are +yielded in the order they were enumerated by the Froidure-Pin algorithm. + +!!! note + This function triggers a full enumeration on the first call + (via [`length`](@ref Base.length)). + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +for x in S + println(x) +end +elts = collect(S) # Vector{Transf{UInt8}} of length 6 +``` + +# See also +- [`getindex`](@ref Base.getindex) +- [`length`](@ref Base.length) +""" +function Base.iterate(fp::FroidurePin, state::Int = 1) + if state > length(fp) + return nothing + end + return (fp[state], state + 1) +end + +""" + Base.eltype(::Type{FroidurePin{E}}) where E -> Type + +Return the element type `E` of a [`FroidurePin{E}`](@ref Semigroups.FroidurePin). + +# Complexity +Constant. +""" +Base.eltype(::Type{FroidurePin{E}}) where {E} = E + +Base.IteratorSize(::Type{<:FroidurePin}) = Base.HasLength() + +# ============================================================================ +# Copy +# ============================================================================ + +""" + Base.copy(fp::FroidurePin{E}) -> FroidurePin{E} + +Create an independent copy of the [`FroidurePin{E}`](@ref Semigroups.FroidurePin) instance. + +This function constructs a new [`FroidurePin{E}`](@ref Semigroups.FroidurePin) from the +generators of `fp`. The returned instance is fully independent of `fp` +and can be enumerated or modified without affecting the original. + +!!! note + This function does not trigger any enumeration of `fp`. + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +T = copy(S) +length(T) # 6, independently computed +``` + +# See also +- [`copy_closure`](@ref) +- [`copy_add_generators`](@ref) +""" +function Base.copy(fp::FroidurePin{E}) where {E} + gens = [generator(fp, i) for i = 1:number_of_generators(fp)] + return FroidurePin(gens) +end + +# ============================================================================ +# Display +# ============================================================================ + +""" + Base.show(io::IO, fp::FroidurePin) + +Display a human-readable summary of the [`FroidurePin`](@ref Semigroups.FroidurePin) +instance `fp` to the I/O stream `io`. + +!!! note + This function does not trigger any enumeration. +""" +function Base.show(io::IO, fp::FroidurePin) + print(io, @wrap_libsemigroups_call LibSemigroups.to_human_readable_repr(fp.cxx_obj)) +end + +# ============================================================================ +# Containment +# ============================================================================ + +""" + Base.in(x::E, fp::FroidurePin{E}) where E -> Bool + +Test membership of an element. + +This function returns `true` if `x` belongs to `fp` and `false` if it +does not. + +# Arguments +- `x::E`: an element to test for membership. +- `fp::FroidurePin{E}`: the FroidurePin instance. + +!!! note + This function may trigger a (partial) enumeration. + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +Transf([2, 1, 3]) in S # true +Transf([1, 1, 1]) in S # false +``` + +# See also +- [`position`](@ref) +- [`current_position`](@ref) +""" +function Base.in(x::E, fp::FroidurePin{E}) where {E} + cxx_x = _cxx_element(x) + return @wrap_libsemigroups_call LibSemigroups.contains(fp.cxx_obj, cxx_x) +end + +# BMat8 dispatch: BMat8Allocated <: BMat8, so we need a fallback +function Base.in(x::BMat8, fp::FroidurePin{BMat8}) + cxx_x = _cxx_element(x) + return @wrap_libsemigroups_call LibSemigroups.contains(fp.cxx_obj, cxx_x) +end + +""" + position(fp::FroidurePin{E}, x::E) where E -> Union{Int, UNDEFINED} + +Find the position of an element with enumeration if necessary. + +This function returns the 1-based position of `x` in `fp`, or +[`UNDEFINED`](@ref Semigroups.UNDEFINED) if `x` is not an element of `fp`. + +# Arguments +- `fp::FroidurePin{E}`: the FroidurePin instance. +- `x::E`: an element whose position is sought. + +!!! note + This function triggers a full enumeration. + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +position(S, Transf([2, 1, 3])) # 1-based index +``` + +# See also +- [`current_position`](@ref) +- [`sorted_position`](@ref) +- [`in`](@ref Base.in) +""" +function position(fp::FroidurePin{E}, x::E) where {E} + cxx_x = _cxx_element(x) + raw = @wrap_libsemigroups_call LibSemigroups.position(fp.cxx_obj, cxx_x) + return _from_cpp(raw) +end + +function position(fp::FroidurePin{BMat8}, x::BMat8) + cxx_x = _cxx_element(x) + raw = @wrap_libsemigroups_call LibSemigroups.position(fp.cxx_obj, cxx_x) + return _from_cpp(raw) +end + +""" + sorted_position(fp::FroidurePin{E}, x::E) where E -> Union{Int, UNDEFINED} + +Return the sorted index of an element. + +This function returns the 1-based position of `x` in the elements of +`fp` when they are sorted, or [`UNDEFINED`](@ref Semigroups.UNDEFINED) +if `x` is not an element of `fp`. + +# Arguments +- `fp::FroidurePin{E}`: the FroidurePin instance. +- `x::E`: an element whose sorted position is sought. + +!!! note + This function triggers a full enumeration. + +# See also +- [`current_position`](@ref) +- [`position`](@ref) +- [`to_sorted_position`](@ref) +- [`sorted_at`](@ref) +""" +function sorted_position(fp::FroidurePin{E}, x::E) where {E} + cxx_x = _cxx_element(x) + raw = @wrap_libsemigroups_call LibSemigroups.sorted_position(fp.cxx_obj, cxx_x) + return _from_cpp(raw) +end + +function sorted_position(fp::FroidurePin{BMat8}, x::BMat8) + cxx_x = _cxx_element(x) + raw = @wrap_libsemigroups_call LibSemigroups.sorted_position(fp.cxx_obj, cxx_x) + return _from_cpp(raw) +end + +""" + to_sorted_position(fp::FroidurePin, i::Integer) -> Union{Int, UNDEFINED} + +Return the sorted index of an element via its index. + +This function returns the 1-based position of the element with index `i` +when the elements are sorted, or [`UNDEFINED`](@ref Semigroups.UNDEFINED) +if `i` is greater than [`length`](@ref Base.length)`(fp)`. + +# Arguments +- `fp::FroidurePin`: the FroidurePin instance. +- `i::Integer`: the 1-based index of the element. + +!!! note + This function triggers a full enumeration. + +# See also +- [`sorted_position`](@ref) +- [`sorted_at`](@ref) +""" +function to_sorted_position(fp::FroidurePin, i::Integer) + idx = _to_cpp(i, UInt) + raw = @wrap_libsemigroups_call LibSemigroups.to_sorted_position(fp.cxx_obj, idx) + return _from_cpp(raw) +end + +""" + current_position(fp::FroidurePin{E}, x::E) where E -> Union{Int, UNDEFINED} + +Find the position of an element with no enumeration. + +This function returns the 1-based position of the element `x` in `fp` +if it is already known to belong to `fp`, or +[`UNDEFINED`](@ref Semigroups.UNDEFINED) if not. If `fp` is not yet +fully enumerated, then this function may return +[`UNDEFINED`](@ref Semigroups.UNDEFINED) even when `x` does belong to `fp`. + +# Arguments +- `fp::FroidurePin{E}`: the FroidurePin instance. +- `x::E`: an element whose position is sought. + +!!! note + This function does not trigger any enumeration. + +# See also +- [`position`](@ref) +- [`sorted_position`](@ref) +""" +function current_position(fp::FroidurePin{E}, x::E) where {E} + cxx_x = _cxx_element(x) + raw = @wrap_libsemigroups_call LibSemigroups.current_position(fp.cxx_obj, cxx_x) + return _from_cpp(raw) +end + +function current_position(fp::FroidurePin{BMat8}, x::BMat8) + cxx_x = _cxx_element(x) + raw = @wrap_libsemigroups_call LibSemigroups.current_position(fp.cxx_obj, cxx_x) + return _from_cpp(raw) +end + +""" + current_position(fp::FroidurePin, w::AbstractVector{<:Integer}) -> Union{Int, UNDEFINED} + +Return the position corresponding to a word. + +This function returns the 1-based position of the element corresponding +to the word `w` (a vector of 1-based generator indices). No enumeration +is performed, and [`UNDEFINED`](@ref Semigroups.UNDEFINED) is returned +if the word does not currently correspond to an element. + +# Arguments +- `fp::FroidurePin`: the FroidurePin instance. +- `w::AbstractVector{<:Integer}`: a word in the generators (1-based + generator indices). + +# Throws +- `LibsemigroupsError`: if any letter in `w` is out of range (exceeds + [`number_of_generators`](@ref)`(fp)`). + +# Complexity +``O(n)`` where ``n`` is the length of the word `w`. + +!!! note + This function does not trigger any enumeration. + +# See also +- [`position`](@ref) +- [`current_position`](@ref current_position(::FroidurePin{E}, ::E) where E) +""" +function current_position(fp::FroidurePin, w::AbstractVector{<:Integer}) + cw = _word_to_cpp(w) + raw = @wrap_libsemigroups_call LibSemigroups.current_position(fp.cxx_obj, cw) + return _from_cpp(raw) +end + +# ============================================================================ +# Modification +# ============================================================================ + +""" + Base.push!(fp::FroidurePin{E}, x::E) where E -> FroidurePin{E} + +Add a new generator `x` to the semigroup `fp`. + +Returns `fp` for method chaining. + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3])) +push!(S, Transf([2, 3, 1])) +length(S) # 6 +``` +""" +function Base.push!(fp::FroidurePin{E}, x::E) where {E} + cxx_x = _cxx_element(x) + @wrap_libsemigroups_call LibSemigroups.add_generator!(fp.cxx_obj, cxx_x) + return fp +end + +function Base.push!(fp::FroidurePin{BMat8}, x::BMat8) + cxx_x = _cxx_element(x) + @wrap_libsemigroups_call LibSemigroups.add_generator!(fp.cxx_obj, cxx_x) + return fp +end + +""" + closure!(fp::FroidurePin{E}, x::E) where E -> FroidurePin{E} + +Add element `x` to the semigroup `fp` if it is not already contained, +and re-enumerate. + +Returns `fp` for method chaining. +""" +function closure!(fp::FroidurePin{E}, x::E) where {E} + cxx_x = _cxx_element(x) + @wrap_libsemigroups_call LibSemigroups.closure!(fp.cxx_obj, cxx_x) + return fp +end + +function closure!(fp::FroidurePin{BMat8}, x::BMat8) + cxx_x = _cxx_element(x) + @wrap_libsemigroups_call LibSemigroups.closure!(fp.cxx_obj, cxx_x) + return fp +end + +""" + copy_closure(fp::FroidurePin{E}, x::E) where E -> FroidurePin{E} + +Return a new `FroidurePin{E}` that is a copy of `fp` with element `x` +added as a generator (if not already contained), and re-enumerated. +""" +function copy_closure(fp::FroidurePin{E}, x::E) where {E} + cxx_x = _cxx_element(x) + new_cxx = @wrap_libsemigroups_call LibSemigroups.copy_closure(fp.cxx_obj, cxx_x) + return FroidurePin{E}(new_cxx) +end + +function copy_closure(fp::FroidurePin{BMat8}, x::BMat8) + cxx_x = _cxx_element(x) + new_cxx = @wrap_libsemigroups_call LibSemigroups.copy_closure(fp.cxx_obj, cxx_x) + return FroidurePin{BMat8}(new_cxx) +end + +""" + copy_add_generators(fp::FroidurePin{E}, x::E) where E -> FroidurePin{E} + +Return a new `FroidurePin{E}` that is a copy of `fp` with element `x` +added as a generator (without checking containment). +""" +function copy_add_generators(fp::FroidurePin{E}, x::E) where {E} + cxx_x = _cxx_element(x) + new_cxx = @wrap_libsemigroups_call LibSemigroups.copy_add_generators(fp.cxx_obj, cxx_x) + return FroidurePin{E}(new_cxx) +end + +function copy_add_generators(fp::FroidurePin{BMat8}, x::BMat8) + cxx_x = _cxx_element(x) + new_cxx = @wrap_libsemigroups_call LibSemigroups.copy_add_generators(fp.cxx_obj, cxx_x) + return FroidurePin{BMat8}(new_cxx) +end + +""" + reserve!(fp::FroidurePin, n::Integer) -> FroidurePin + +Pre-allocate storage for at least `n` elements. This is a performance +hint and does not affect correctness. + +Returns `fp` for method chaining. +""" +function reserve!(fp::FroidurePin, n::Integer) + val = UInt(n) + @wrap_libsemigroups_call LibSemigroups.reserve!(fp.cxx_obj, val) + return fp +end + +# ============================================================================ +# Settings +# ============================================================================ + +""" + batch_size(fp::FroidurePin) -> Int + +Return the current value of the batch size. + +The batch size is the minimum number of new elements to be found by any +call to [`run!`](@ref). This is used by, for example, +[`position`](@ref) so that it is possible to find the position of an +element after only partially enumerating the semigroup. + +The default value of the batch size is `8192`. + +# Complexity +Constant. + +# See also +- [`set_batch_size!`](@ref) +""" +batch_size(fp::FroidurePin) = Int(LibSemigroups.batch_size(fp.cxx_obj)) + +""" + set_batch_size!(fp::FroidurePin, n::Integer) -> FroidurePin + +Set a new value for the batch size. + +The *batch size* is the number of new elements to be found by any call +to [`run!`](@ref). This is used by, for example, +[`position`](@ref) so that it is possible to find the position of an +element after only partially enumerating the semigroup. + +The default value of the batch size is `8192`. + +Returns `fp` for method chaining. + +# Arguments +- `fp::FroidurePin`: the FroidurePin instance. +- `n::Integer`: the new value for the batch size. + +# Complexity +Constant. + +# See also +- [`batch_size`](@ref) +""" +function set_batch_size!(fp::FroidurePin, n::Integer) + LibSemigroups.set_batch_size!(fp.cxx_obj, UInt(n)) + return fp +end + +# ============================================================================ +# Predicates +# ============================================================================ + +""" + contains_one(fp::FroidurePin) -> Bool + +Check if the multiplicative identity is an element of `fp`. + +This function returns `true` if the identity element (as returned by the +`One` adapter for the element type) is an element of the semigroup +represented by `fp`. + +# Complexity +At worst ``O(|S| n)`` where ``S`` is the semigroup represented by `fp` +and ``n`` is the number of generators. + +!!! note + This function triggers a full enumeration. + +# See also +- [`currently_contains_one`](@ref) +""" +contains_one(fp::FroidurePin) = LibSemigroups.contains_one(fp.cxx_obj) + +""" + is_idempotent(fp::FroidurePin, i::Integer) -> Bool + +Check if the element at 1-based position `i` is an idempotent. + +This function returns `true` if the element in position `i` is an +idempotent (i.e., `x * x == x`) and `false` if it is not. + +# Arguments +- `fp::FroidurePin`: the FroidurePin instance. +- `i::Integer`: the 1-based index of the element. + +# Throws +- `LibsemigroupsError`: if `i` is less than `1` or greater than the + size of `fp`. + +!!! note + This function triggers a full enumeration. +""" +function is_idempotent(fp::FroidurePin, i::Integer) + idx = _to_cpp(i, UInt) + return @wrap_libsemigroups_call LibSemigroups.is_idempotent(fp.cxx_obj, idx) +end + +# ============================================================================ +# Index queries (FroidurePinBase methods — uint32_t positions) +# ============================================================================ + +""" + prefix(fp::FroidurePin, i::Integer) -> Int + +Return the position of the longest proper prefix. + +This function returns the 1-based position of the prefix of the element +`x` at 1-based position `i`, where the prefix is the element of length +one less than `x` obtained by removing the last letter of the +factorisation. + +For generators (elements of length 1), the prefix is +[`UNDEFINED`](@ref). + +# Arguments +- `fp::FroidurePin`: the FroidurePin instance. +- `i::Integer`: the 1-based position of the element. + +# Throws +- `LibsemigroupsError`: if `i` is less than `1` or greater than or + equal to [`current_size`](@ref). + +# Complexity +Constant. + +!!! note + This function does not trigger any enumeration. +""" +function prefix(fp::FroidurePin, i::Integer) + idx = _to_cpp(i, UInt32) + raw = @wrap_libsemigroups_call LibSemigroups.prefix(fp.cxx_obj, idx) + return _from_cpp(raw) +end + +""" + suffix(fp::FroidurePin, i::Integer) -> Int + +Return the position of the longest proper suffix. + +This function returns the 1-based position of the suffix of the element +`x` at 1-based position `i`, where the suffix is the element of length +one less than `x` obtained by removing the first letter of the +factorisation. + +For generators (elements of length 1), the suffix is +[`UNDEFINED`](@ref). + +# Arguments +- `fp::FroidurePin`: the FroidurePin instance. +- `i::Integer`: the 1-based position of the element. + +# Throws +- `LibsemigroupsError`: if `i` is less than `1` or greater than or + equal to [`current_size`](@ref). + +# Complexity +Constant. + +!!! note + This function does not trigger any enumeration. +""" +function suffix(fp::FroidurePin, i::Integer) + idx = _to_cpp(i, UInt32) + raw = @wrap_libsemigroups_call LibSemigroups.suffix(fp.cxx_obj, idx) + return _from_cpp(raw) +end + +""" + first_letter(fp::FroidurePin, i::Integer) -> Int + +Return the first letter of the element with specified index. + +This function returns the 1-based index of the generator corresponding +to the first letter of the element in position `i`. + +# Arguments +- `fp::FroidurePin`: the FroidurePin instance. +- `i::Integer`: the 1-based position of the element. + +# Throws +- `LibsemigroupsError`: if `i` is less than `1` or greater than or + equal to [`current_size`](@ref). + +# Complexity +Constant. + +!!! note + `generator(fp, first_letter(fp, i))` is only equal to `fp[first_letter(fp, i)]` + if there are no duplicate generators. + +!!! note + This function does not trigger any enumeration. +""" +function first_letter(fp::FroidurePin, i::Integer) + idx = _to_cpp(i, UInt32) + raw = @wrap_libsemigroups_call LibSemigroups.first_letter(fp.cxx_obj, idx) + return _from_cpp(raw) +end + +""" + final_letter(fp::FroidurePin, i::Integer) -> Int + +Return the last letter of the element with specified index. + +This function returns the 1-based index of the generator corresponding +to the final letter of the element in position `i`. + +# Arguments +- `fp::FroidurePin`: the FroidurePin instance. +- `i::Integer`: the 1-based position of the element. + +# Throws +- `LibsemigroupsError`: if `i` is less than `1` or greater than or + equal to [`current_size`](@ref). + +# Complexity +Constant. + +!!! note + `generator(fp, final_letter(fp, i))` is only equal to `fp[final_letter(fp, i)]` + if there are no duplicate generators. + +!!! note + This function does not trigger any enumeration. +""" +function final_letter(fp::FroidurePin, i::Integer) + idx = _to_cpp(i, UInt32) + raw = @wrap_libsemigroups_call LibSemigroups.final_letter(fp.cxx_obj, idx) + return _from_cpp(raw) +end + +""" + fast_product(fp::FroidurePin, i::Integer, j::Integer) -> Int + +Return the 1-based position of the product of the elements at 1-based +positions `i` and `j`. + +The semigroup must be fully enumerated before calling this method. +""" +function fast_product(fp::FroidurePin, i::Integer, j::Integer) + ci = _to_cpp(i, UInt) + cj = _to_cpp(j, UInt) + raw = @wrap_libsemigroups_call LibSemigroups.fast_product(fp.cxx_obj, ci, cj) + return _from_cpp(raw) +end + +""" + number_of_rules(fp::FroidurePin) -> Int + +Return the total number of rules (relations) in the semigroup. + +Triggers full enumeration if not already complete. +""" +number_of_rules(fp::FroidurePin) = Int(LibSemigroups.number_of_rules(fp.cxx_obj)) + +""" + current_number_of_rules(fp::FroidurePin) -> Int + +Return the number of rules discovered so far (without triggering +further enumeration). +""" +current_number_of_rules(fp::FroidurePin) = + Int(LibSemigroups.current_number_of_rules(fp.cxx_obj)) + +""" + number_of_idempotents(fp::FroidurePin) -> Int + +Return the total number of idempotent elements in the semigroup. + +Triggers full enumeration if not already complete. +""" +number_of_idempotents(fp::FroidurePin) = + Int(LibSemigroups.number_of_idempotents(fp.cxx_obj)) + +""" + currently_contains_one(fp::FroidurePin) -> Bool + +Check whether the identity element has been discovered so far +(without triggering further enumeration). +""" +currently_contains_one(fp::FroidurePin) = LibSemigroups.currently_contains_one(fp.cxx_obj) + +""" + current_max_word_length(fp::FroidurePin) -> Int + +Return the maximum word length of elements enumerated so far +(without triggering further enumeration). +""" +current_max_word_length(fp::FroidurePin) = + Int(LibSemigroups.current_max_word_length(fp.cxx_obj)) + +""" + number_of_elements_of_length(fp::FroidurePin, len::Integer) -> Int + +Return the number of elements whose minimal factorisation has +exactly length `len`. +""" +function number_of_elements_of_length(fp::FroidurePin, len::Integer) + return Int(LibSemigroups.number_of_elements_of_length(fp.cxx_obj, UInt(len))) +end + +""" + number_of_elements_of_length(fp::FroidurePin, min::Integer, max::Integer) -> Int + +Return the number of elements whose minimal factorisation has +length in the range `[min, max)`. +""" +function number_of_elements_of_length(fp::FroidurePin, min::Integer, max::Integer) + return Int( + LibSemigroups.number_of_elements_of_length_range(fp.cxx_obj, UInt(min), UInt(max)), + ) +end + +""" + position_of_generator(fp::FroidurePin, i::Integer) -> Int + +Return the 1-based position of the `i`-th generator (1-based) in the +enumerated elements. +""" +function position_of_generator(fp::FroidurePin, i::Integer) + idx = _to_cpp(i, UInt32) + raw = @wrap_libsemigroups_call LibSemigroups.position_of_generator(fp.cxx_obj, idx) + return _from_cpp(raw) +end + +""" + current_length(fp::FroidurePin, i::Integer) -> Int + +Return the length of the minimal factorisation of the element at +1-based position `i` (without triggering further enumeration). +""" +function current_length(fp::FroidurePin, i::Integer) + idx = _to_cpp(i, UInt32) + raw = @wrap_libsemigroups_call LibSemigroups.current_length(fp.cxx_obj, idx) + return Int(raw) +end + +""" + word_length(fp::FroidurePin, i::Integer) -> Int + +Return the length of the minimal factorisation of the element at +1-based position `i`. + +Triggers full enumeration if not already complete. + +Named `word_length` to avoid conflict with `Base.length`. +""" +function word_length(fp::FroidurePin, i::Integer) + idx = _to_cpp(i, UInt32) + raw = @wrap_libsemigroups_call LibSemigroups.length(fp.cxx_obj, idx) + return Int(raw) +end + +""" + product_by_reduction(fp::FroidurePin, i::Integer, j::Integer) -> Int + +Return the 1-based position of the product of the elements at 1-based +positions `i` and `j`, computed using the Cayley graph (no full +enumeration required, but the elements at positions `i` and `j` must +already be enumerated). +""" +function product_by_reduction(fp::FroidurePin, i::Integer, j::Integer) + ci = _to_cpp(i, UInt32) + cj = _to_cpp(j, UInt32) + raw = @wrap_libsemigroups_call LibSemigroups.product_by_reduction(fp.cxx_obj, ci, cj) + return _from_cpp(raw) +end + +# ============================================================================ +# Collections — rules, normal forms, idempotents, sorted elements +# ============================================================================ + +""" + rules(fp::FroidurePin) -> Vector{Pair{Vector{Int}, Vector{Int}}} + +Return all defining rules of the semigroup as a vector of `lhs => rhs` +pairs, where each side is a 1-based generator-index word. + +Triggers full enumeration if not already complete. + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +rs = rules(S) +for (lhs, rhs) in rs + println(lhs, " => ", rhs) +end +``` +""" +function rules(fp::FroidurePin) + lhs_raw = @wrap_libsemigroups_call LibSemigroups.rules_lhs(fp.cxx_obj) + rhs_raw = @wrap_libsemigroups_call LibSemigroups.rules_rhs(fp.cxx_obj) + n = length(lhs_raw) + result = Vector{Pair{Vector{Int},Vector{Int}}}(undef, n) + for i = 1:n + result[i] = _word_from_cpp(lhs_raw[i]) => _word_from_cpp(rhs_raw[i]) + end + return result +end + +""" + current_rules(fp::FroidurePin) -> Vector{Pair{Vector{Int}, Vector{Int}}} + +Return all rules discovered so far (without triggering further enumeration) +as a vector of `lhs => rhs` pairs with 1-based generator indices. +""" +function current_rules(fp::FroidurePin) + lhs_raw = @wrap_libsemigroups_call LibSemigroups.current_rules_lhs(fp.cxx_obj) + rhs_raw = @wrap_libsemigroups_call LibSemigroups.current_rules_rhs(fp.cxx_obj) + n = length(lhs_raw) + result = Vector{Pair{Vector{Int},Vector{Int}}}(undef, n) + for i = 1:n + result[i] = _word_from_cpp(lhs_raw[i]) => _word_from_cpp(rhs_raw[i]) + end + return result +end + +""" + normal_forms(fp::FroidurePin) -> Vector{Vector{Int}} + +Return the normal forms (canonical representatives) for all elements, +as 1-based generator-index words. + +Triggers full enumeration if not already complete. +""" +function normal_forms(fp::FroidurePin) + raw = @wrap_libsemigroups_call LibSemigroups.normal_forms(fp.cxx_obj) + return [_word_from_cpp(w) for w in raw] +end + +""" + current_normal_forms(fp::FroidurePin) -> Vector{Vector{Int}} + +Return the normal forms discovered so far (without triggering further +enumeration) as 1-based generator-index words. +""" +function current_normal_forms(fp::FroidurePin) + raw = @wrap_libsemigroups_call LibSemigroups.current_normal_forms(fp.cxx_obj) + return [_word_from_cpp(w) for w in raw] +end + +""" + idempotents(fp::FroidurePin{E}) -> Vector{E} + +Return all idempotent elements of the semigroup (elements `x` such +that `x * x == x`). + +Triggers full enumeration if not already complete. + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +ids = idempotents(S) # [Transf([1, 2, 3])] +``` +""" +function idempotents(fp::FroidurePin{E}) where {E} + raw = @wrap_libsemigroups_call LibSemigroups.idempotents(fp.cxx_obj) + # GC.@preserve raw to keep StdVector alive while iterating; + # _copy_cxx_element converts Dereferenced refs to Allocated copies. + GC.@preserve raw begin + return E[_wrap_element(E, _copy_cxx_element(x)) for x in raw] + end +end + +""" + sorted_elements(fp::FroidurePin{E}) -> Vector{E} + +Return all elements of the semigroup in sorted order. + +Triggers full enumeration if not already complete. + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +se = sorted_elements(S) +``` +""" +function sorted_elements(fp::FroidurePin{E}) where {E} + raw = @wrap_libsemigroups_call LibSemigroups.sorted_elements(fp.cxx_obj) + # GC.@preserve raw to keep StdVector alive while iterating; + # _copy_cxx_element converts Dereferenced refs to Allocated copies. + GC.@preserve raw begin + return E[_wrap_element(E, _copy_cxx_element(x)) for x in raw] + end +end + +# ============================================================================ +# Factorisations +# ============================================================================ + +""" + minimal_factorisation(fp::FroidurePin, i::Integer) -> Vector{Int} + +Return the minimal factorisation of the element at 1-based position `i` +as a 1-based generator-index word. + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +w = minimal_factorisation(S, 1) # [1] or [2] — a single generator +``` +""" +function minimal_factorisation(fp::FroidurePin, i::Integer) + idx = _to_cpp(i, UInt32) + raw = @wrap_libsemigroups_call LibSemigroups.minimal_factorisation(fp.cxx_obj, idx) + return _word_from_cpp(raw) +end + +""" + current_minimal_factorisation(fp::FroidurePin, i::Integer) -> Vector{Int} + +Return the minimal factorisation of the element at 1-based position `i` +without triggering further enumeration. +""" +function current_minimal_factorisation(fp::FroidurePin, i::Integer) + idx = _to_cpp(i, UInt32) + raw = @wrap_libsemigroups_call LibSemigroups.current_minimal_factorisation( + fp.cxx_obj, + idx, + ) + return _word_from_cpp(raw) +end + +""" + factorisation(fp::FroidurePin, i::Integer) -> Vector{Int} + +Return the factorisation of the element at 1-based position `i` +as a 1-based generator-index word. + +This may not be the minimal factorisation. +""" +function factorisation(fp::FroidurePin, i::Integer) + idx = _to_cpp(i, UInt32) + raw = @wrap_libsemigroups_call LibSemigroups.factorisation(fp.cxx_obj, idx) + return _word_from_cpp(raw) +end + +# ============================================================================ +# Word-position queries +# ============================================================================ + +""" + position(fp::FroidurePin, w::AbstractVector{<:Integer}) -> Int + +Return the 1-based position of the element represented by the 1-based +generator-index word `w`. + +Triggers full enumeration if not already complete. + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +position(S, [1]) # position of generator 1 +``` +""" +function position(fp::FroidurePin, w::AbstractVector{<:Integer}) + cw = _word_to_cpp(w) + raw = @wrap_libsemigroups_call LibSemigroups.position(fp.cxx_obj, cw) + return _from_cpp(raw) +end + +# ============================================================================ +# Cayley graphs +# ============================================================================ + +""" + right_cayley_graph(fp::FroidurePin) -> WordGraph + +Return the right Cayley graph of the semigroup. + +Triggers full enumeration if not already complete. +""" +right_cayley_graph(fp::FroidurePin) = LibSemigroups.right_cayley_graph(fp.cxx_obj) + +""" + current_right_cayley_graph(fp::FroidurePin) -> WordGraph + +Return the right Cayley graph for elements enumerated so far. +""" +current_right_cayley_graph(fp::FroidurePin) = + LibSemigroups.current_right_cayley_graph(fp.cxx_obj) + +""" + left_cayley_graph(fp::FroidurePin) -> WordGraph + +Return the left Cayley graph of the semigroup. + +Triggers full enumeration if not already complete. +""" +left_cayley_graph(fp::FroidurePin) = LibSemigroups.left_cayley_graph(fp.cxx_obj) + +""" + current_left_cayley_graph(fp::FroidurePin) -> WordGraph + +Return the left Cayley graph for elements enumerated so far. +""" +current_left_cayley_graph(fp::FroidurePin) = + LibSemigroups.current_left_cayley_graph(fp.cxx_obj) + +# ============================================================================ +# Word-element conversion +# ============================================================================ + +""" + to_element(fp::FroidurePin{E}, w::AbstractVector{<:Integer}) -> E + +Convert a 1-based generator-index word `w` to the corresponding element. + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +x = to_element(S, [1, 2]) # product of generators 1 and 2 +``` +""" +function to_element(fp::FroidurePin{E}, w::AbstractVector{<:Integer}) where {E} + cw = _word_to_cpp(w) + raw = @wrap_libsemigroups_call LibSemigroups.to_element(fp.cxx_obj, cw) + return _wrap_element(E, raw) +end + +""" + equal_to(fp::FroidurePin, w1::AbstractVector{<:Integer}, w2::AbstractVector{<:Integer}) -> Bool + +Check whether two 1-based generator-index words represent the same element. + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +equal_to(S, [1, 1], [1]) # does gen1*gen1 == gen1? +``` +""" +function equal_to( + fp::FroidurePin, + w1::AbstractVector{<:Integer}, + w2::AbstractVector{<:Integer}, +) + cw1 = _word_to_cpp(w1) + cw2 = _word_to_cpp(w2) + return @wrap_libsemigroups_call LibSemigroups.equal_to(fp.cxx_obj, cw1, cw2) +end diff --git a/src/transf.jl b/src/transf.jl index 0431c87..8f5c3cd 100644 --- a/src/transf.jl +++ b/src/transf.jl @@ -447,6 +447,7 @@ Base.:(<)(t1::Transf, t2::Transf) = LibSemigroups.is_less(t1.cxx_obj, t2.cxx_obj Base.:(<=)(t1::Transf, t2::Transf) = LibSemigroups.is_less_equal(t1.cxx_obj, t2.cxx_obj) Base.:(>)(t1::Transf, t2::Transf) = LibSemigroups.is_greater(t1.cxx_obj, t2.cxx_obj) Base.:(>=)(t1::Transf, t2::Transf) = LibSemigroups.is_greater_equal(t1.cxx_obj, t2.cxx_obj) +Base.isless(t1::Transf, t2::Transf) = t1 < t2 # Hash Base.hash(t::Transf, h::UInt) = hash(hash_value(t.cxx_obj), h) @@ -796,6 +797,7 @@ Base.:(<)(p1::PPerm, p2::PPerm) = LibSemigroups.is_less(p1.cxx_obj, p2.cxx_obj) Base.:(<=)(p1::PPerm, p2::PPerm) = LibSemigroups.is_less_equal(p1.cxx_obj, p2.cxx_obj) Base.:(>)(p1::PPerm, p2::PPerm) = LibSemigroups.is_greater(p1.cxx_obj, p2.cxx_obj) Base.:(>=)(p1::PPerm, p2::PPerm) = LibSemigroups.is_greater_equal(p1.cxx_obj, p2.cxx_obj) +Base.isless(p1::PPerm, p2::PPerm) = p1 < p2 # Hash Base.hash(p::PPerm, h::UInt) = hash(hash_value(p.cxx_obj), h) @@ -1145,6 +1147,7 @@ Base.:(<)(p1::Perm, p2::Perm) = LibSemigroups.is_less(p1.cxx_obj, p2.cxx_obj) Base.:(<=)(p1::Perm, p2::Perm) = LibSemigroups.is_less_equal(p1.cxx_obj, p2.cxx_obj) Base.:(>)(p1::Perm, p2::Perm) = LibSemigroups.is_greater(p1.cxx_obj, p2.cxx_obj) Base.:(>=)(p1::Perm, p2::Perm) = LibSemigroups.is_greater_equal(p1.cxx_obj, p2.cxx_obj) +Base.isless(p1::Perm, p2::Perm) = p1 < p2 # Hash Base.hash(p::Perm, h::UInt) = hash(hash_value(p.cxx_obj), h) diff --git a/src/word-graph.jl b/src/word-graph.jl index 8ac2c76..91e8dcc 100644 --- a/src/word-graph.jl +++ b/src/word-graph.jl @@ -100,7 +100,7 @@ Constant. See also [`out_degree`](@ref), [`add_nodes!`](@ref). """ -number_of_nodes(g::WordGraph) = Int(LibSemigroups.number_of_nodes(g)) +@cxxdereference number_of_nodes(g::WordGraph) = Int(LibSemigroups.number_of_nodes(g)) """ out_degree(g::WordGraph) -> Int @@ -117,7 +117,7 @@ Constant. See also [`number_of_nodes`](@ref), [`target`](@ref). """ -out_degree(g::WordGraph) = Int(LibSemigroups.out_degree(g)) +@cxxdereference out_degree(g::WordGraph) = Int(LibSemigroups.out_degree(g)) """ target(g::WordGraph, source::Integer, label::Integer) -> Union{Int, UndefinedType} @@ -142,7 +142,7 @@ Constant. See also [`target!`](@ref), [`is_undefined`](@ref Semigroups.is_undefined). """ -function target(g::WordGraph, source::Integer, label::Integer) +@cxxdereference function target(g::WordGraph, source::Integer, label::Integer) # Do _to_cpp conversion outside @wrap_libsemigroups_call so its # InexactError (from zero / negative inputs) propagates as-is rather than # being re-wrapped as LibsemigroupsError. @@ -241,6 +241,6 @@ end # Display # ============================================================================ -function Base.show(io::IO, g::WordGraph) +@cxxdereference function Base.show(io::IO, g::WordGraph) print(io, "WordGraph($(number_of_nodes(g)), $(out_degree(g)))") end diff --git a/test/runtests.jl b/test/runtests.jl index 9682068..214bd89 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -22,4 +22,5 @@ using Semigroups include("test_presentation.jl") include("test_presentation_examples.jl") include("test_word_range.jl") + include("test_froidure_pin_transf.jl") end diff --git a/test/test_bmat8.jl b/test/test_bmat8.jl index 4d2718c..78bd8e6 100644 --- a/test/test_bmat8.jl +++ b/test/test_bmat8.jl @@ -111,10 +111,12 @@ using Semigroups @testset "random" begin - @test minimum_dim(random(BMat8, 4)) == 4 + # random(BMat8, 4) has nonzero entries only in the top-left 4×4 block, + # so minimum_dim is at most 4 (can be less if random rows/cols are zero) + @test minimum_dim(random(BMat8, 4)) <= 4 x = BMat8([[1, 0, 1], [0, 1, 0], [0, 0, 0]]) - @test minimum_dim(random(x, 4)) == 4 + @test minimum_dim(random(x, 4)) <= 4 @test_throws LibsemigroupsError random(x, 0) @test_throws LibsemigroupsError random(BMat8, 0) diff --git a/test/test_froidure_pin_transf.jl b/test/test_froidure_pin_transf.jl new file mode 100644 index 0000000..e761c92 --- /dev/null +++ b/test/test_froidure_pin_transf.jl @@ -0,0 +1,1479 @@ +# Copyright (c) 2026, James W. Swent +# +# Distributed under the terms of the GPL license version 3. +# +# The full license is in the file LICENSE, distributed with this software. + +""" +test_froidure_pin_transf.jl - FroidurePin> correctness tests + +Ported from libsemigroups/tests/test-froidure-pin-transf.cpp. +All indices are 1-based (Julia convention). +""" + +# ============================================================================ +# Helper functions (mirroring C++ test helpers) +# ============================================================================ + +""" +Test that element at position `pos` in `S` is idempotent: + 1. is_idempotent(S, pos) returns true + 2. x * x == x + 3. fast_product(S, pos, pos) == pos +""" +function test_idempotent(S, x) + pos = Semigroups.position(S, x) + @test is_idempotent(S, pos) + @test x * x == x + @test fast_product(S, pos, pos) == pos +end + +""" +Test that current_rules are consistent: for each rule (lhs, rhs), +the current_position of both sides should be equal, and the count +should match current_number_of_rules. +""" +function test_current_rules_iterator(S) + nr = 0 + for (lhs, rhs) in current_rules(S) + @test current_position(S, lhs) == current_position(S, rhs) + nr += 1 + end + @test nr == current_number_of_rules(S) +end + +ReportGuard(false) do + + @testset "FroidurePin correctness" begin + + # ----------------------------------------------------------------------- + # Test 042: "JDM favourite" [standard] + # Large semigroup: 597369 elements, 8 generators of degree 8 + # ----------------------------------------------------------------------- + @testset "042: JDM favourite [standard]" begin + S = FroidurePin( + Transf([2, 8, 3, 7, 1, 5, 2, 6]), + Transf([3, 5, 7, 2, 5, 6, 3, 8]), + Transf([4, 1, 8, 3, 5, 7, 3, 5]), + Transf([4, 3, 4, 5, 6, 4, 1, 2]), + Transf([5, 4, 8, 8, 5, 6, 1, 5]), + Transf([6, 7, 4, 1, 4, 1, 6, 2]), + Transf([7, 1, 2, 2, 2, 7, 4, 5]), + Transf([8, 8, 5, 1, 7, 5, 2, 8]), + ) + reserve!(S, 597369) + + @test length(S) == 597369 + @test number_of_idempotents(S) == 8194 + + # Position of every element matches iteration order + for (i, x) in Base.enumerate(S) + @test Semigroups.position(S, x) == i + end + + # add_generators increases size + push!(S, Transf([8, 2, 3, 7, 8, 5, 2, 6])) + @test length(S) == 826713 + + # closure with already-present generator doesn't change size + closure!(S, Transf([8, 2, 3, 7, 8, 5, 2, 6])) + @test length(S) == 826713 + + # minimal_factorisation (1-based: C++ position 10 → Julia 11) + @test minimal_factorisation(S, 11) == [1, 3] + @test S[11] == Transf([1, 5, 8, 3, 4, 5, 1, 7]) + @test_throws LibsemigroupsError minimal_factorisation(S, 1000000001) + + # Every idempotent satisfies x * x == x + @test all(x * x == x for x in idempotents(S)) + @test length(idempotents(S)) == number_of_idempotents(S) + + # Sorted elements are strictly increasing + @test issorted(sorted_elements(S)) + end + + # ----------------------------------------------------------------------- + # Test 043: "no exception zero generators given" [quick] + # ----------------------------------------------------------------------- + @testset "043: no exception zero generators" begin + # The C++ library accepts empty generator sets; the Julia wrapper + # currently requires at least one generator, so we test that + # constraint. + @test_throws ErrorException FroidurePin(Transf{UInt16}[]) + end + + # ----------------------------------------------------------------------- + # Test 044: "exception generators of different degrees" [quick] + # ----------------------------------------------------------------------- + @testset "044: exception generators of different degrees" begin + S = FroidurePin(Transf([3, 5, 7, 2, 5, 6, 3, 8, 4])) # degree 9 + @test_throws LibsemigroupsError push!(S, Transf([2, 8, 3, 7, 1, 1, 2, 3])) # degree 8 + end + + # ----------------------------------------------------------------------- + # Test 045: "exception current_position" [quick] + # ----------------------------------------------------------------------- + @testset "045: exception current_position" begin + U = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + # Empty word → identity → position 1 (1-based) + @test current_position(U, Int[]) == 1 + # Valid word, no throw (may return UNDEFINED if not yet enumerated) + @test_nowarn current_position(U, [1, 1, 2, 3]) + # Generator index 6 out of range for 5 generators + @test_throws LibsemigroupsError current_position(U, [6]) + end + + # ----------------------------------------------------------------------- + # Test 046: "exception to_element" [quick] + # ----------------------------------------------------------------------- + @testset "046: exception to_element" begin + U = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + # Empty word → identity element == generator(1) + @test to_element(U, Int[]) == generator(U, 1) + # Out-of-range generator index + @test_throws LibsemigroupsError to_element(U, [6]) + + # to_element([1,1,2,3]) == gen1 * gen1 * gen2 * gen3 + gen1 = Transf([1, 2, 3, 4, 5, 6]) + gen2 = Transf([2, 1, 3, 4, 5, 6]) + gen3 = Transf([5, 1, 2, 3, 4, 6]) + u = to_element(U, [1, 1, 2, 3]) + @test u == gen1 * gen1 * gen2 * gen3 + end + + # ----------------------------------------------------------------------- + # Test 047: "exception gens" [quick] + # ----------------------------------------------------------------------- + @testset "047: exception gens" begin + for i = 1:19 + # Build cyclic shift transformations of degree i (1-based) + gens = [Transf([mod(k + j - 2, i) + 1 for k = 1:i]) for j = 1:i] + S = FroidurePin(gens) + for j = 1:i + @test generator(S, j) isa Transf + end + @test_throws Exception generator(S, i + 1) + end + end + + # ----------------------------------------------------------------------- + # Test 048: "exception prefix" [quick] + # ----------------------------------------------------------------------- + @testset "048: exception prefix" begin + U = FroidurePin( + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + for i = 1:length(U) + # prefix returns UNDEFINED for generators, valid position otherwise + @test_nowarn prefix(U, i) + end + @test_throws Exception prefix(U, length(U) + 1) + end + + # ----------------------------------------------------------------------- + # Test 049: "exception suffix" [quick] + # ----------------------------------------------------------------------- + @testset "049: exception suffix" begin + U = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + @test length(U) == 7776 + + for i = 1:length(U) + # suffix returns UNDEFINED for generators, valid position otherwise + @test_nowarn suffix(U, i) + end + @test_throws Exception suffix(U, length(U) + 1) + end + + # ----------------------------------------------------------------------- + # Test 050: "exception first_letter" [quick] + # ----------------------------------------------------------------------- + @testset "050: exception first_letter" begin + U = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + for i = 1:length(U) + @test first_letter(U, i) isa Integer + @test_throws Exception first_letter(U, i + length(U)) + end + end + + # ----------------------------------------------------------------------- + # Test 051: "exception final_letter" [quick] + # ----------------------------------------------------------------------- + @testset "051: exception final_letter" begin + U = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + for i = 1:length(U) + @test final_letter(U, i) isa Integer + @test_throws Exception final_letter(U, i + length(U)) + end + end + + # ----------------------------------------------------------------------- + # Test 052: "exception current_length" [quick] + # ----------------------------------------------------------------------- + @testset "052: exception current_length" begin + U = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + for i = 1:length(U) + @test current_length(U, i) isa Integer + @test_throws Exception current_length(U, i + length(U)) + end + end + + # ----------------------------------------------------------------------- + # Test 053: "exception product_by_reduction" [quick] + # ----------------------------------------------------------------------- + @testset "053: exception product_by_reduction" begin + U = FroidurePin(Transf([1, 2, 3, 4]), Transf([4, 2, 2, 3])) + + n = length(U) + for i = 1:n + for j = 1:n + @test product_by_reduction(U, i, j) isa Integer + @test_throws Exception product_by_reduction(U, i + n, j) + @test_throws Exception product_by_reduction(U, i, j + n) + @test_throws Exception product_by_reduction(U, i + n, j + n) + end + end + end + + # ----------------------------------------------------------------------- + # Test 054: "exception fast_product" [quick] + # ----------------------------------------------------------------------- + @testset "054: exception fast_product" begin + U = FroidurePin(Transf([1, 2, 3, 4]), Transf([4, 2, 2, 3])) + + n = length(U) + for i = 1:n + for j = 1:n + @test fast_product(U, i, j) isa Integer + @test_throws Exception fast_product(U, i + n, j) + @test_throws Exception fast_product(U, i, j + n) + @test_throws Exception fast_product(U, i + n, j + n) + end + end + end + + # ----------------------------------------------------------------------- + # Test 055: "exception current_position" [quick] + # (Tests position_of_generator bounds) + # ----------------------------------------------------------------------- + @testset "055: exception position_of_generator" begin + for i = 1:19 + gens = [Transf([mod(k + j - 2, i) + 1 for k = 1:i]) for j = 1:i] + S = FroidurePin(gens) + for j = 1:i + @test position_of_generator(S, j) isa Integer + end + @test_throws Exception position_of_generator(S, i + 1) + end + end + + # ----------------------------------------------------------------------- + # Test 056: "exception is_idempotent" [quick] + # ----------------------------------------------------------------------- + @testset "056: exception is_idempotent" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([6, 2, 4, 4, 3, 6]), + Transf([3, 2, 3, 4, 5, 5]), + Transf([6, 6, 3, 2, 2, 3]), + ) + + @test length(S) == 441 + for i = 1:441 + @test is_idempotent(S, i) isa Bool + end + for i = 0:19 + @test_throws Exception is_idempotent(S, 442 + i) + end + end + + # ----------------------------------------------------------------------- + # Test 057: "exception add_generators" [quick] + # ----------------------------------------------------------------------- + @testset "057: exception add_generators" begin + T = FroidurePin( + Transf([2, 8, 3, 7, 1, 1, 2, 3]), + Transf([3, 5, 7, 2, 5, 6, 3, 8]), + ) + + # Adding generators of same degree succeeds + push!(T, Transf([2, 3, 3, 3, 2, 2, 4, 5])) + push!(T, Transf([2, 3, 2, 4, 2, 5, 2, 6])) + @test number_of_generators(T) == 4 + + # Adding generator with wrong degree throws + @test_throws LibsemigroupsError push!(T, Transf([2, 3, 2, 4, 2, 5, 2, 6, 2])) + end + + # ----------------------------------------------------------------------- + # Test 058: "number_of_idempotents" [quick] + # ----------------------------------------------------------------------- + @testset "058: number_of_idempotents" begin + S = FroidurePin( + Transf([2, 8, 3, 7, 1, 1, 2, 3]), + Transf([3, 5, 7, 2, 5, 6, 3, 8]), + ) + @test number_of_idempotents(S) == 72 + end + + # ----------------------------------------------------------------------- + # Test 059: "small semigroup" [quick] + # ----------------------------------------------------------------------- + @testset "059: small semigroup" begin + S = FroidurePin(Transf([1, 2, 1]), Transf([1, 2, 3])) + + @test length(S) == 2 + @test degree(S) == 3 + @test number_of_idempotents(S) == 2 + @test number_of_generators(S) == 2 + @test number_of_rules(S) == 4 + + @test S[1] == Transf([1, 2, 1]) + @test S[2] == Transf([1, 2, 3]) + + @test Semigroups.position(S, Transf([1, 2, 1])) == 1 + @test Transf([1, 2, 1]) in S + + @test Semigroups.position(S, Transf([1, 2, 3])) == 2 + @test Transf([1, 2, 3]) in S + + @test Semigroups.position(S, Transf([1, 1, 1])) == UNDEFINED + @test !(Transf([1, 1, 1]) in S) + end + + # ----------------------------------------------------------------------- + # Test 060: "large semigroup" [quick] + # ----------------------------------------------------------------------- + @testset "060: large semigroup" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + @test length(S) == 7776 + @test degree(S) == 6 + @test number_of_idempotents(S) == 537 + @test number_of_generators(S) == 5 + @test number_of_rules(S) == 2459 + end + + # ----------------------------------------------------------------------- + # Test 061: "at, position, current_*" [quick] + # NOTE: Julia's getindex calls length() which triggers full enumeration, + # so we test partial enumeration via enumerate! and verify element values + # after full enumeration. + # ----------------------------------------------------------------------- + @testset "061: at, position, current_*" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + set_batch_size!(S, 1024) + + # Test partial enumeration via enumerate! + enumerate!(S, 1029) + @test current_size(S) == 1029 + @test current_number_of_rules(S) == 74 + @test current_max_word_length(S) == 7 + + # Element value checks (after full enum via getindex) + @test S[101] == Transf([6, 4, 5, 2, 3, 6]) + @test Semigroups.position(S, Transf([6, 4, 5, 2, 3, 6])) == 101 + + @test S[1024] == Transf([6, 5, 4, 5, 2, 6]) + @test Semigroups.position(S, Transf([6, 5, 4, 5, 2, 6])) == 1024 + + @test S[3001] == Transf([6, 4, 6, 4, 5, 6]) + @test Semigroups.position(S, Transf([6, 4, 6, 4, 5, 6])) == 3001 + + @test length(S) == 7776 + @test degree(S) == 6 + @test number_of_idempotents(S) == 537 + @test number_of_generators(S) == 5 + @test number_of_rules(S) == 2459 + end + + # ----------------------------------------------------------------------- + # Test 062: "run" [quick] + # ----------------------------------------------------------------------- + @testset "062: run (incremental enumerate)" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + set_batch_size!(S, 1024) + + enumerate!(S, 3000) + @test current_size(S) == 3000 + @test current_number_of_rules(S) == 526 + @test current_max_word_length(S) == 9 + + enumerate!(S, 3001) + @test current_size(S) == 4024 + @test current_number_of_rules(S) == 999 + @test current_max_word_length(S) == 10 + + enumerate!(S, 7000) + @test current_size(S) == 7000 + @test current_number_of_rules(S) == 2044 + @test current_max_word_length(S) == 12 + + @test length(S) == 7776 + end + + # ----------------------------------------------------------------------- + # Test 063: "run [many stops and starts]" [quick] + # ----------------------------------------------------------------------- + @testset "063: run many stops and starts" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + set_batch_size!(S, 128) + + i = 1 + while !finished(S) + enumerate!(S, i * 128) + i += 1 + end + + @test length(S) == 7776 + @test number_of_idempotents(S) == 537 + @test number_of_generators(S) == 5 + @test number_of_rules(S) == 2459 + end + + # ----------------------------------------------------------------------- + # Test 064: "factorisation, length [1 element]" [quick] + # ----------------------------------------------------------------------- + @testset "064: factorisation, length [1 element]" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + set_batch_size!(S, 1024) + + # C++ position 5537 → Julia 5538; C++ word {1,2,2,2,3,2,4,1,2,2,3} → Julia [2,3,3,3,4,3,5,2,3,3,4] + w = factorisation(S, 5538) + @test w == [2, 3, 3, 3, 4, 3, 5, 2, 3, 3, 4] + @test current_length(S, 5538) == 11 + @test word_length(S, 5538) == 11 + + @test current_size(S) == 5539 + @test current_number_of_rules(S) == 1484 + + @test word_length(S, 7776) == 16 + end + + # ----------------------------------------------------------------------- + # Test 065: "factorisation, products [all elements]" [quick] + # ----------------------------------------------------------------------- + @testset "065: factorisation roundtrip [all elements]" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + for i = 1:length(S) + w = factorisation(S, i) + @test current_position(S, w) == i + end + end + + # ----------------------------------------------------------------------- + # Test 066: "first/final letter, prefix, suffix, products" [quick] + # ----------------------------------------------------------------------- + @testset "066: first/final letter, prefix, suffix" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + run!(S) + + # Element 6378 (C++ 6377, 1-based) + @test first_letter(S, 6378) == 3 + @test prefix(S, 6378) == 5050 + @test final_letter(S, 6378) == 3 + @test suffix(S, 6378) == 5150 + @test fast_product(S, prefix(S, 6378), final_letter(S, 6378)) == 6378 + @test fast_product(S, first_letter(S, 6378), suffix(S, 6378)) == 6378 + @test product_by_reduction(S, prefix(S, 6378), final_letter(S, 6378)) == 6378 + @test product_by_reduction(S, first_letter(S, 6378), suffix(S, 6378)) == 6378 + + # Element 2104 (C++ 2103) + @test first_letter(S, 2104) == 4 + @test prefix(S, 2104) == 1051 + @test final_letter(S, 2104) == 2 + @test suffix(S, 2104) == 861 + @test fast_product(S, prefix(S, 2104), final_letter(S, 2104)) == 2104 + @test fast_product(S, first_letter(S, 2104), suffix(S, 2104)) == 2104 + + # Element 3408 (C++ 3407) + @test first_letter(S, 3408) == 3 + @test prefix(S, 3408) == 1924 + @test final_letter(S, 3408) == 4 + @test suffix(S, 3408) == 2116 + + # Element 4246 (C++ 4245) + @test first_letter(S, 4246) == 3 + @test prefix(S, 4246) == 2768 + @test final_letter(S, 4246) == 4 + @test suffix(S, 4246) == 2320 + + # Element 3684 (C++ 3683) + @test first_letter(S, 3684) == 5 + @test prefix(S, 3684) == 2247 + @test final_letter(S, 3684) == 3 + @test suffix(S, 3684) == 1686 + + # Element 1 (C++ 0): generator — prefix/suffix are UNDEFINED + @test first_letter(S, 1) == 1 + @test prefix(S, 1) == UNDEFINED + @test final_letter(S, 1) == 1 + @test suffix(S, 1) == UNDEFINED + + # Element 7776 (C++ 7775): last element + @test first_letter(S, 7776) == 2 + @test prefix(S, 7776) == 7761 + @test final_letter(S, 7776) == 3 + @test suffix(S, 7776) == 7769 + end + + # ----------------------------------------------------------------------- + # Test 067: "current_position [standard]" [quick] + # ----------------------------------------------------------------------- + @testset "067: position_of_generator" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + for i = 1:5 + @test position_of_generator(S, i) == i + end + end + + # ----------------------------------------------------------------------- + # Test 068: "current_position [duplicate gens]" [quick] + # ----------------------------------------------------------------------- + @testset "068: duplicate generators" begin + # Build semigroup with many duplicate generators using push! + S = FroidurePin(Transf([1, 2, 3, 4, 5, 6])) # gen 1: identity + push!(S, Transf([2, 1, 3, 4, 5, 6])) # gen 2: (1,2) + for _ = 1:10 + push!(S, Transf([2, 1, 3, 4, 5, 6])) # gen 3-12: dups + end + push!(S, Transf([6, 2, 3, 4, 5, 6])) # gen 13: distinct + for _ = 1:8 + push!(S, Transf([2, 1, 3, 4, 5, 6])) # gen 14-21: dups + end + push!(S, Transf([5, 1, 2, 3, 4, 6])) # gen 22: distinct + for _ = 1:9 + push!(S, Transf([2, 1, 3, 4, 5, 6])) # gen 23-31: dups + end + push!(S, Transf([2, 2, 3, 4, 5, 6])) # gen 32: distinct + + # Duplicates map to same element position + @test position_of_generator(S, 1) == 1 # identity + @test position_of_generator(S, 2) == 2 # (1,2) + @test position_of_generator(S, 3) == 2 # dup → same position + @test position_of_generator(S, 11) == 2 # dup → same position + @test number_of_generators(S) == 32 + @test length(S) == 7776 + end + + # ----------------------------------------------------------------------- + # Test 069: "current_position [after add_generators]" [quick] + # ----------------------------------------------------------------------- + @testset "069: incremental add_generators" begin + S = FroidurePin(Transf([1, 2, 3, 4, 5, 6])) # identity + @test length(S) == 1 + @test number_of_rules(S) == 1 + + push!(S, Transf([2, 1, 3, 4, 5, 6])) + @test length(S) == 2 + @test number_of_rules(S) == 4 + + push!(S, Transf([5, 1, 2, 3, 4, 6])) + @test length(S) == 120 + @test number_of_rules(S) == 25 + + push!(S, Transf([6, 2, 3, 4, 5, 6])) + @test length(S) == 1546 + @test number_of_rules(S) == 495 + + push!(S, Transf([2, 2, 3, 4, 5, 6])) + @test length(S) == 7776 + @test number_of_rules(S) == 2459 + + # Generator positions shift as elements are added (1-based) + @test position_of_generator(S, 1) == 1 + @test position_of_generator(S, 2) == 2 + @test position_of_generator(S, 3) == 3 + @test position_of_generator(S, 4) == 121 # added when size was 120 + @test position_of_generator(S, 5) == 1547 # added when size was 1546 + end + + # ----------------------------------------------------------------------- + # Test 070: "cbegin_idempotents/cend" [quick] + # ----------------------------------------------------------------------- + @testset "070: idempotent iteration with test_idempotent" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + run!(S) + + nr = 0 + for x in idempotents(S) + test_idempotent(S, x) + nr += 1 + end + @test nr == number_of_idempotents(S) + end + + # Test 071: same as 070, skipped (functionally identical) + + # ----------------------------------------------------------------------- + # Test 072: "is_idempotent" [quick] + # ----------------------------------------------------------------------- + @testset "072: is_idempotent counting" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + nr = count(i -> is_idempotent(S, i), 1:length(S)) + @test nr == number_of_idempotents(S) + @test nr == 537 + end + + # ----------------------------------------------------------------------- + # Test 073: "cbegin_idempotents/cend, is_idempotent" [standard] + # Large degree-7 semigroup with 6322 idempotents + # ----------------------------------------------------------------------- + @testset "073: idempotents [standard, degree 7]" begin + S = FroidurePin( + Transf([2, 3, 4, 5, 6, 7, 1]), # cyclic shift + Transf([2, 1, 3, 4, 5, 6, 7]), # (1,2) + Transf([1, 2, 3, 4, 5, 6, 1]), # collapse 7→1 + ) + + ids = idempotents(S) + @test length(ids) == number_of_idempotents(S) + @test length(ids) == 6322 + + for x in ids + test_idempotent(S, x) + end + + # Second pass gives same count (repeatability) + ids2 = idempotents(S) + @test length(ids2) == 6322 + end + + # ----------------------------------------------------------------------- + # Test 074: "finished, started" [quick] + # ----------------------------------------------------------------------- + @testset "074: finished, started" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + @test !started(S) + @test !finished(S) + + set_batch_size!(S, 1024) + enumerate!(S, 10) + @test started(S) + @test !finished(S) + + enumerate!(S, 8000) + @test started(S) + @test finished(S) + end + + # ----------------------------------------------------------------------- + # Test 075: "current_position" [quick] + # ----------------------------------------------------------------------- + @testset "075: current_position (element)" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + # Generators have sequential positions + for i = 1:5 + @test current_position(S, generator(S, i)) == i + end + + set_batch_size!(S, 1024) + enumerate!(S, 1024) + @test current_size(S) == 1029 + + # Element within enumerated range + @test current_position(S, Transf([6, 2, 6, 6, 3, 6])) == 1029 + + # Wrong degree → UNDEFINED + @test current_position(S, Transf([6, 2, 6, 6, 3, 6, 7])) == UNDEFINED + + # Not yet enumerated → UNDEFINED from current_position + @test current_position(S, Transf([6, 5, 6, 2, 1, 6])) == UNDEFINED + + # But full position() finds it (triggers enumeration) + @test Semigroups.position(S, Transf([6, 5, 6, 2, 1, 6])) == 1030 + end + + # ----------------------------------------------------------------------- + # Test 076: "sorted_position, sorted_at" [quick] + # ----------------------------------------------------------------------- + @testset "076: sorted_position, sorted_at" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + # Generator sorted positions (C++ 0-based → Julia 1-based: +1) + @test sorted_position(S, generator(S, 1)) == 311 + @test sorted_at(S, 311) == generator(S, 1) + + @test sorted_position(S, generator(S, 2)) == 1391 + @test sorted_at(S, 1391) == generator(S, 2) + + @test sorted_position(S, generator(S, 3)) == 5236 + @test sorted_at(S, 5236) == generator(S, 3) + + @test sorted_position(S, generator(S, 4)) == 6791 + @test sorted_at(S, 6791) == generator(S, 4) + + @test sorted_position(S, generator(S, 5)) == 1607 + @test sorted_at(S, 1607) == generator(S, 5) + + @test finished(S) + + # Position-to-sorted conversion (C++ 1024 → Julia 1025) + @test to_sorted_position(S, 1025) == 6811 + @test sorted_at(S, 6811) == S[1025] + + @test sorted_position(S, Transf([6, 2, 6, 6, 3, 6])) == 6909 + @test sorted_at(S, 6909) == Transf([6, 2, 6, 6, 3, 6]) + + # Wrong degree → UNDEFINED + @test sorted_position(S, Transf([6, 6, 6, 2, 6, 6, 7])) == UNDEFINED + + # Out of bounds + @test_throws Exception sorted_at(S, 100001) + @test_throws BoundsError S[100001] + @test to_sorted_position(S, 100001) == UNDEFINED + end + + # ----------------------------------------------------------------------- + # Test 077: "right/left Cayley graph" [quick] + # ----------------------------------------------------------------------- + @testset "077: Cayley graph consistency" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + run!(S) + + rcg = right_cayley_graph(S) + lcg = left_cayley_graph(S) + + # Identity self-loops + @test target(rcg, 1, 1) == 1 + @test target(lcg, 1, 1) == 1 + + # Cayley graph consistency: for every element x and generator g, + # position(x * g) == right_cayley_graph target(pos(x), gen_idx) + # position(g * x) == left_cayley_graph target(pos(x), gen_idx) + for i = 1:length(S) + x = S[i] + for g = 1:5 + @test Semigroups.position(S, x * generator(S, g)) == target(rcg, i, g) + @test Semigroups.position(S, generator(S, g) * x) == target(lcg, i, g) + end + end + end + + # ----------------------------------------------------------------------- + # Test 078: "iterator" [quick] + # NOTE: Julia's iterate triggers full enumeration via length(), + # so pre-enumeration iteration is not directly testable. + # We verify that iteration covers all elements and containment. + # ----------------------------------------------------------------------- + @testset "078: iterator" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + elts = collect(S) + @test length(elts) == 7776 + # All elements are contained + for x in elts + @test x in S + end + end + + # Test 079-080, 082: C++ iterator arithmetic — skipped (Julia iteration is sequential) + + # ----------------------------------------------------------------------- + # Test 081: "iterator sorted" [quick] + # ----------------------------------------------------------------------- + @testset "081: sorted iteration consistency" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + se = sorted_elements(S) + @test finished(S) + @test length(se) == length(S) + + for (i, x) in Base.enumerate(se) + @test sorted_position(S, x) == i + @test to_sorted_position(S, Semigroups.position(S, x)) == i + end + end + + # ----------------------------------------------------------------------- + # Test 083: "copy [not enumerated]" [quick] + # ----------------------------------------------------------------------- + @testset "083: copy" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + T = copy(S) + @test number_of_generators(T) == 5 + @test degree(T) == 6 + @test length(T) == 7776 + @test number_of_idempotents(T) == 537 + @test number_of_rules(T) == 2459 + end + + # ----------------------------------------------------------------------- + # Test 084: "copy_closure [not enumerated]" [quick] + # ----------------------------------------------------------------------- + @testset "084: copy_closure" begin + S = FroidurePin(Transf([1, 2, 3, 4, 5, 6]), Transf([2, 1, 3, 4, 5, 6])) + + # copy_closure adds 3 more generators + T = copy_closure(S, Transf([5, 1, 2, 3, 4, 6])) + T = copy_closure(T, Transf([6, 2, 3, 4, 5, 6])) + T = copy_closure(T, Transf([2, 2, 3, 4, 5, 6])) + + @test number_of_generators(T) == 5 + @test length(T) == 7776 + @test number_of_idempotents(T) == 537 + @test number_of_rules(T) == 2459 + end + + # ----------------------------------------------------------------------- + # Test 085: "copy_add_generators [not enumerated]" [quick] + # ----------------------------------------------------------------------- + @testset "085: copy_add_generators" begin + S = FroidurePin(Transf([1, 2, 3, 4, 5, 6]), Transf([2, 1, 3, 4, 5, 6])) + + T = copy_add_generators(S, Transf([5, 1, 2, 3, 4, 6])) + T = copy_add_generators(T, Transf([6, 2, 3, 4, 5, 6])) + T = copy_add_generators(T, Transf([2, 2, 3, 4, 5, 6])) + + @test number_of_generators(T) == 5 + @test length(T) == 7776 + @test number_of_idempotents(T) == 537 + @test number_of_rules(T) == 2459 + end + + # ----------------------------------------------------------------------- + # Test 086: "copy [partly enumerated]" [quick] + # NOTE: Julia copy() reconstructs from generators (starts fresh). + # We verify it produces the same semigroup. + # ----------------------------------------------------------------------- + @testset "086: copy produces same semigroup" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + T = copy(S) + @test number_of_generators(T) == 5 + @test degree(T) == 6 + @test length(T) == 7776 + @test number_of_idempotents(T) == 537 + @test number_of_rules(T) == 2459 + end + + # ----------------------------------------------------------------------- + # Test 087: "copy_closure [partly enumerated]" [quick] + # ----------------------------------------------------------------------- + @testset "087: copy_closure from partly enumerated" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + ) + set_batch_size!(S, 60) + enumerate!(S, 60) + @test current_size(S) == 63 + + T = copy_closure(S, Transf([6, 2, 3, 4, 5, 6])) + T = copy_closure(T, Transf([2, 2, 3, 4, 5, 6])) + + @test generator(T, 4) == Transf([6, 2, 3, 4, 5, 6]) + @test generator(T, 5) == Transf([2, 2, 3, 4, 5, 6]) + @test number_of_generators(T) == 5 + @test length(T) == 7776 + end + + # ----------------------------------------------------------------------- + # Test 088: "copy_add_generators [partly enumerated]" [quick] + # ----------------------------------------------------------------------- + @testset "088: copy_add_generators from partly enumerated" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + ) + set_batch_size!(S, 60) + enumerate!(S, 60) + + T = copy_add_generators(S, Transf([6, 2, 3, 4, 5, 6])) + T = copy_add_generators(T, Transf([2, 2, 3, 4, 5, 6])) + + @test number_of_generators(T) == 5 + @test length(T) == 7776 + @test number_of_idempotents(T) == 537 + end + + # ----------------------------------------------------------------------- + # Test 089: "copy [fully enumerated]" [quick] + # ----------------------------------------------------------------------- + @testset "089: copy from fully enumerated" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + run!(S) + @test finished(S) + + T = copy(S) + @test length(T) == 7776 + @test number_of_idempotents(T) == 537 + @test number_of_rules(T) == 2459 + end + + # ----------------------------------------------------------------------- + # Test 090: "copy_closure [fully enumerated]" [quick] + # ----------------------------------------------------------------------- + @testset "090: copy_closure from fully enumerated" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + ) + run!(S) + @test finished(S) + @test length(S) == 120 # S₅ + + T = copy_closure(S, Transf([6, 2, 3, 4, 5, 6])) + T = copy_closure(T, Transf([2, 2, 3, 4, 5, 6])) + + @test number_of_generators(T) == 5 + @test length(T) == 7776 + @test number_of_idempotents(T) == 537 + end + + # ----------------------------------------------------------------------- + # Test 091: "copy_add_generators [fully enumerated]" [quick] + # ----------------------------------------------------------------------- + @testset "091: copy_add_generators from fully enumerated" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + ) + run!(S) + @test length(S) == 120 + + T = copy_add_generators(S, Transf([6, 2, 3, 4, 5, 6])) + T = copy_add_generators(T, Transf([2, 2, 3, 4, 5, 6])) + + @test number_of_generators(T) == 5 + @test length(T) == 7776 + @test number_of_idempotents(T) == 537 + end + + # ----------------------------------------------------------------------- + # Test 092: "rules [duplicate gens]" [quick] + # ----------------------------------------------------------------------- + @testset "092: rules with duplicate generators" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), # gen 1: identity + Transf([1, 2, 3, 4, 5, 6]), # gen 2: dup of gen 1 + Transf([2, 1, 3, 4, 5, 6]), # gen 3: (1,2) + Transf([2, 1, 3, 4, 5, 6]), # gen 4: dup of gen 3 + Transf([5, 1, 2, 3, 4, 6]), # gen 5 + ) + run!(S) + + rs = rules(S) + # First rule: gen 2 == gen 1 (duplicate) + @test rs[1] == ([2] => [1]) + # Second rule: gen 4 == gen 3 (duplicate) + @test rs[2] == ([4] => [3]) + # Total rules count is consistent + @test number_of_rules(S) == length(rs) + end + + # ----------------------------------------------------------------------- + # Test 093: "rules" [quick] + # ----------------------------------------------------------------------- + @testset "093: rules before/during/after enumeration" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + # No current rules before enumeration + @test length(current_rules(S)) == 0 + + # Partial enumeration to discover rules + set_batch_size!(S, 10) + enumerate!(S, 10) + @test !finished(S) + + # First two rules (1-based generators): identity^2=identity, identity*gen2=gen2 + cr = current_rules(S) + @test length(cr) >= 2 + @test cr[1] == ([1, 1] => [1]) + @test cr[2] == ([1, 2] => [2]) + test_current_rules_iterator(S) + + run!(S) + @test finished(S) + @test number_of_rules(S) == 2459 + + # Rules still consistent after full enumeration + test_current_rules_iterator(S) + end + + # ----------------------------------------------------------------------- + # Test 094: "rules [copy_closure, duplicate gens]" [quick] + # ----------------------------------------------------------------------- + @testset "094: rules copy_closure with duplicate gens" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([1, 2, 3, 4, 5, 6]), # dup + Transf([2, 1, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), # dup + Transf([5, 1, 2, 3, 4, 6]), + ) + run!(S) + @test length(S) == 120 + @test number_of_rules(S) == 33 + + T = copy_closure(S, Transf([6, 2, 3, 4, 5, 6])) + T = copy_closure(T, Transf([1, 2, 3, 4, 5, 6])) # dup identity + T = copy_closure(T, Transf([2, 1, 3, 4, 5, 6])) # dup (1,2) + T = copy_closure(T, Transf([2, 2, 3, 4, 5, 6])) + + @test length(T) == 7776 + @test number_of_idempotents(T) == 537 + end + + # ----------------------------------------------------------------------- + # Test 095: "rules [copy_add_generators, duplicate gens]" [quick] + # ----------------------------------------------------------------------- + @testset "095: rules copy_add_generators with duplicate gens" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + ) + run!(S) + + T = copy_add_generators(S, Transf([6, 2, 3, 4, 5, 6])) + T = copy_add_generators(T, Transf([2, 2, 3, 4, 5, 6])) + + @test length(T) == 7776 + end + + # ----------------------------------------------------------------------- + # Tests 096-098: rules from copy at various enumeration states + # ----------------------------------------------------------------------- + @testset "096-098: rules from copy" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + + # Copy before enumeration + T = copy(S) + run!(T) + @test finished(T) + test_current_rules_iterator(T) + @test number_of_rules(T) == 2459 + + # Copy after full enumeration + run!(S) + T2 = copy(S) + run!(T2) + @test number_of_rules(T2) == number_of_rules(S) + test_current_rules_iterator(T2) + end + + # ----------------------------------------------------------------------- + # Tests 099-102: rules from copy_closure / copy_add_generators + # ----------------------------------------------------------------------- + @testset "099-102: rules from copy_closure/add_generators" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + ) + + # copy_closure + T = copy_closure(S, Transf([6, 2, 3, 4, 5, 6])) + T = copy_closure(T, Transf([2, 2, 3, 4, 5, 6])) + test_current_rules_iterator(T) + @test length(T) == 7776 + @test number_of_rules(T) == 2459 + + # copy_add_generators + T2 = copy_add_generators(S, Transf([6, 2, 3, 4, 5, 6])) + T2 = copy_add_generators(T2, Transf([2, 2, 3, 4, 5, 6])) + @test number_of_rules(T2) == 2459 + test_current_rules_iterator(T2) + end + + # ----------------------------------------------------------------------- + # Tests 103-104: rules from copy_closure/add_generators [fully enum] + # ----------------------------------------------------------------------- + @testset "103-104: rules from copy ops fully enumerated" begin + S = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + ) + run!(S) + @test finished(S) + + T = copy_closure(S, Transf([6, 2, 3, 4, 5, 6])) + T = copy_closure(T, Transf([2, 2, 3, 4, 5, 6])) + test_current_rules_iterator(T) + @test number_of_rules(T) == 2459 + + T2 = copy_add_generators(S, Transf([6, 2, 3, 4, 5, 6])) + T2 = copy_add_generators(T2, Transf([2, 2, 3, 4, 5, 6])) + @test number_of_rules(T2) == 2459 + test_current_rules_iterator(T2) + end + + # ----------------------------------------------------------------------- + # Test 105: "add_generators [duplicate generators]" [quick] + # ----------------------------------------------------------------------- + @testset "105: add_generators with duplicates" begin + S = FroidurePin( + Transf([1, 2, 1, 4, 5, 6]), + Transf([1, 2, 1, 4, 5, 6]), # duplicate + ) + @test length(S) == 1 + @test number_of_generators(S) == 2 + + # Add duplicate again + push!(S, generator(S, 1)) + @test length(S) == 1 + @test number_of_generators(S) == 3 + + # Add identity + push!(S, Transf([1, 2, 3, 4, 5, 6])) + @test length(S) == 2 + + # Incremental growth + push!(S, Transf([1, 2, 4, 6, 6, 5])) + @test length(S) == 7 + + push!(S, Transf([2, 1, 3, 5, 5, 6])) + @test length(S) == 18 + + push!(S, Transf([5, 4, 4, 2, 1, 6])) + @test length(S) == 87 + + push!(S, Transf([5, 4, 6, 2, 1, 6])) + @test length(S) == 97 + + push!(S, Transf([6, 6, 3, 4, 5, 1])) + @test length(S) == 119 + + # Add product of existing elements (already in S) + push!(S, Transf([2, 1, 3, 5, 5, 6]) * Transf([5, 4, 4, 2, 1, 6])) + @test length(S) == 119 + @test number_of_generators(S) == 10 + + # position_of_generator tracks positions + @test position_of_generator(S, 1) == 1 + @test position_of_generator(S, 2) == 1 # dup + @test position_of_generator(S, 3) == 1 # dup + @test position_of_generator(S, 4) == 2 # identity + end + + # ----------------------------------------------------------------------- + # Test 108: "closure [duplicate generators]" [quick] + # ----------------------------------------------------------------------- + @testset "108: closure with duplicates" begin + S = FroidurePin( + Transf([1, 2, 1, 4, 5, 6]), + Transf([1, 2, 1, 4, 5, 6]), # duplicate + ) + @test length(S) == 1 + + # Closure with existing element: no new generator added + closure!(S, generator(S, 1)) + @test length(S) == 1 + @test number_of_generators(S) == 2 # unchanged + + # Closure with new elements + closure!(S, Transf([1, 2, 3, 4, 5, 6])) + @test length(S) == 2 + @test number_of_generators(S) == 3 + + closure!(S, Transf([1, 2, 4, 6, 6, 5])) + @test length(S) == 7 + + closure!(S, Transf([2, 1, 3, 5, 5, 6])) + @test length(S) == 18 + + closure!(S, Transf([5, 4, 4, 2, 1, 6])) + @test length(S) == 87 + + closure!(S, Transf([5, 4, 6, 2, 1, 6])) + @test length(S) == 97 + + closure!(S, Transf([6, 6, 3, 4, 5, 1])) + @test length(S) == 119 + @test number_of_generators(S) == 8 # fewer than add_generators (skips dups) + end + + # ----------------------------------------------------------------------- + # Test 109: "closure" [quick] — all T₃ elements + # ----------------------------------------------------------------------- + @testset "109: closure with all T₃" begin + # All 27 transformations of degree 3 (1-based images) + all_t3 = [Transf([a, b, c]) for a = 1:3 for b = 1:3 for c = 1:3] + @test length(all_t3) == 27 + + S = FroidurePin(all_t3[1:1]) + for t in all_t3 + closure!(S, t) + end + @test length(S) == 27 + # closure selects a minimal generating set — fewer than 27 + @test number_of_generators(S) <= 27 + end + + # ----------------------------------------------------------------------- + # Test 110: "factorisation" [quick] + # ----------------------------------------------------------------------- + @testset "110: factorisation" begin + S = FroidurePin(Transf([2, 2, 5, 6, 5, 6]), Transf([3, 4, 3, 4, 6, 6])) + @test factorisation(S, 3) == [1, 2] # 3rd element = gen1 * gen2 + end + + # Tests 111, 114: large semigroup with/without reserve — covered by Test 042 + + # ----------------------------------------------------------------------- + # Test 112: "minimal_factorisation" [quick] + # ----------------------------------------------------------------------- + @testset "112: minimal_factorisation exceptions" begin + S = FroidurePin(Transf([2, 2, 5, 6, 5, 6])) + + @test minimal_factorisation(S, 1) == [1] + @test_throws LibsemigroupsError minimal_factorisation(S, 10000001) + end + + # ----------------------------------------------------------------------- + # Test 113: "batch_size" [quick] + # ----------------------------------------------------------------------- + @testset "113: batch_size large value" begin + S = FroidurePin(Transf([2, 2, 5, 6, 5, 6]), Transf([3, 4, 3, 4, 6, 6])) + run!(S) + @test length(S) == 5 + end + + # ----------------------------------------------------------------------- + # Test 115: "exception: generators of different degrees" [quick] + # NOTE: The 2-arg C++ constructor doesn't validate degree at + # construction. Degree mismatch is caught by add_generator! instead. + # This test verifies the add_generator! path (already covered by 044). + # ----------------------------------------------------------------------- + @testset "115: mixed degree add_generator" begin + S = FroidurePin(Transf([1, 2, 3, 4, 5, 6])) # degree 6 + @test_throws LibsemigroupsError push!(S, Transf([1, 2, 3, 4, 5, 6, 6])) # degree 7 + end + + # ----------------------------------------------------------------------- + # Test 116: "exception: current_position" [quick] (near-duplicate of 045) + # ----------------------------------------------------------------------- + @testset "116: current_position word exceptions" begin + U = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + @test current_position(U, Int[]) == 1 + @test_nowarn current_position(U, [1, 1, 2, 3]) + @test_throws LibsemigroupsError current_position(U, [6]) + end + + # ----------------------------------------------------------------------- + # Test 117: "exception: to_element" [quick] (near-duplicate of 046) + # ----------------------------------------------------------------------- + @testset "117: to_element exceptions" begin + U = FroidurePin( + Transf([1, 2, 3, 4, 5, 6]), + Transf([2, 1, 3, 4, 5, 6]), + Transf([5, 1, 2, 3, 4, 6]), + Transf([6, 2, 3, 4, 5, 6]), + Transf([2, 2, 3, 4, 5, 6]), + ) + @test to_element(U, Int[]) == generator(U, 1) + @test_throws LibsemigroupsError to_element(U, [6]) + @test to_element(U, [1, 1, 2, 3]) == + generator(U, 1) * generator(U, 1) * generator(U, 2) * generator(U, 3) + end + + # ----------------------------------------------------------------------- + # Test 118: "exception: gens, current_position" [quick] + # ----------------------------------------------------------------------- + @testset "118: generator and position_of_generator bounds" begin + for i = 1:19 + gens = [Transf([mod(k + j - 2, i) + 1 for k = 1:i]) for j = 1:i] + S = FroidurePin(gens) + for j = 1:i + @test generator(S, j) isa Transf + @test position_of_generator(S, j) isa Integer + end + @test_throws Exception generator(S, i + 1) + @test_throws Exception position_of_generator(S, i + 1) + end + end + + # ----------------------------------------------------------------------- + # Test 119: "exception: add_generators" [quick] + # ----------------------------------------------------------------------- + @testset "119: add_generators degree exception" begin + S = FroidurePin(Transf([1, 2, 3, 4, 5, 6]), Transf([2, 3, 4, 3, 3, 4])) + push!(S, Transf([1, 2, 3, 4, 4, 4])) # same degree → OK + @test_throws LibsemigroupsError push!(S, Transf([1, 2, 3, 4, 4, 4, 4])) # degree 7 + end + + end # @testset "FroidurePin" + +end # ReportGuard(false)