From 7dabf63438b05ccaf05105d63867aa94065cc9f7 Mon Sep 17 00:00:00 2001 From: James Swent Date: Wed, 22 Apr 2026 21:29:17 +0100 Subject: [PATCH 01/17] feat: scaffold FroidurePinBase C++ binding --- deps/src/CMakeLists.txt | 1 + deps/src/froidure-pin-base.cpp | 79 ++++++++++++++++++++++++++++++++ deps/src/libsemigroups_julia.cpp | 1 + deps/src/libsemigroups_julia.hpp | 1 + 4 files changed, 82 insertions(+) create mode 100644 deps/src/froidure-pin-base.cpp diff --git a/deps/src/CMakeLists.txt b/deps/src/CMakeLists.txt index 6bc4c3a..abc346e 100644 --- a/deps/src/CMakeLists.txt +++ b/deps/src/CMakeLists.txt @@ -46,6 +46,7 @@ add_library(libsemigroups_julia SHARED libsemigroups_julia.cpp bmat8.cpp constants.cpp + froidure-pin-base.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..8aa2088 --- /dev/null +++ b/deps/src/froidure-pin-base.cpp @@ -0,0 +1,79 @@ +// 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 + +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; + + // 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()); + + //////////////////////////////////////////////////////////////////////// + // Minimal methods to prove the type is wired up + //////////////////////////////////////////////////////////////////////// + + // 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(); + }); + } + +} // namespace libsemigroups_julia diff --git a/deps/src/libsemigroups_julia.cpp b/deps/src/libsemigroups_julia.cpp index ba5d9d7..7372324 100644 --- a/deps/src/libsemigroups_julia.cpp +++ b/deps/src/libsemigroups_julia.cpp @@ -40,6 +40,7 @@ namespace libsemigroups_julia { define_order(mod); define_word_range(mod); define_word_graph(mod); + define_froidure_pin_base(mod); define_presentation(mod); define_presentation_examples(mod); } diff --git a/deps/src/libsemigroups_julia.hpp b/deps/src/libsemigroups_julia.hpp index b582894..e4b4f91 100644 --- a/deps/src/libsemigroups_julia.hpp +++ b/deps/src/libsemigroups_julia.hpp @@ -62,6 +62,7 @@ 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_presentation(jl::Module& mod); void define_presentation_examples(jl::Module& mod); From 3b3629d4cac19f6ff8c83f6d336e4e1df6588a5e Mon Sep 17 00:00:00 2001 From: James Swent Date: Wed, 22 Apr 2026 21:36:28 +0100 Subject: [PATCH 02/17] feat: complete FroidurePinBase method bindings --- deps/src/froidure-pin-base.cpp | 352 ++++++++++++++++++++++++++++++++- 1 file changed, 351 insertions(+), 1 deletion(-) diff --git a/deps/src/froidure-pin-base.cpp b/deps/src/froidure-pin-base.cpp index 8aa2088..016f33c 100644 --- a/deps/src/froidure-pin-base.cpp +++ b/deps/src/froidure-pin-base.cpp @@ -24,8 +24,14 @@ #include #include +#include + +#include #include +#include +#include +#include namespace jlcxx { template <> @@ -42,6 +48,7 @@ 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 @@ -50,7 +57,23 @@ namespace libsemigroups_julia { "FroidurePinBase", jlcxx::julia_base_type()); //////////////////////////////////////////////////////////////////////// - // Minimal methods to prove the type is wired up + // 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) @@ -74,6 +97,333 @@ namespace libsemigroups_julia { [](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 From a73fe0f1d0a7b198d904d83192603ba1bcbe69c3 Mon Sep 17 00:00:00 2001 From: James Swent Date: Wed, 22 Apr 2026 21:57:46 +0100 Subject: [PATCH 03/17] feat: bind FroidurePin for all 10 element types --- deps/src/CMakeLists.txt | 1 + deps/src/froidure-pin.cpp | 377 +++++++++++++++++++++++++++++++ deps/src/libsemigroups_julia.cpp | 1 + deps/src/libsemigroups_julia.hpp | 1 + 4 files changed, 380 insertions(+) create mode 100644 deps/src/froidure-pin.cpp diff --git a/deps/src/CMakeLists.txt b/deps/src/CMakeLists.txt index abc346e..37a9edd 100644 --- a/deps/src/CMakeLists.txt +++ b/deps/src/CMakeLists.txt @@ -47,6 +47,7 @@ add_library(libsemigroups_julia SHARED bmat8.cpp constants.cpp froidure-pin-base.cpp + froidure-pin.cpp order.cpp report.cpp runner.cpp diff --git a/deps/src/froidure-pin.cpp b/deps/src/froidure-pin.cpp new file mode 100644 index 0000000..357b0cd --- /dev/null +++ b/deps/src/froidure-pin.cpp @@ -0,0 +1,377 @@ +// 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>> + : std::false_type {}; + template <> + struct IsMirroredType>> + : std::false_type {}; + template <> + struct IsMirroredType>> + : std::false_type {}; + + template <> + struct IsMirroredType>> + : std::false_type {}; + template <> + struct IsMirroredType>> + : std::false_type {}; + template <> + struct IsMirroredType>> + : std::false_type {}; + + template <> + struct IsMirroredType>> + : std::false_type {}; + template <> + struct IsMirroredType>> + : std::false_type {}; + template <> + struct IsMirroredType>> + : 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 7372324..0cd8b6e 100644 --- a/deps/src/libsemigroups_julia.cpp +++ b/deps/src/libsemigroups_julia.cpp @@ -41,6 +41,7 @@ namespace libsemigroups_julia { 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 e4b4f91..3af5852 100644 --- a/deps/src/libsemigroups_julia.hpp +++ b/deps/src/libsemigroups_julia.hpp @@ -63,6 +63,7 @@ namespace libsemigroups_julia { 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); From c853fbc447f0524cdbccf9135321449d5d1b5c57 Mon Sep 17 00:00:00 2001 From: James Swent Date: Wed, 22 Apr 2026 22:23:16 +0100 Subject: [PATCH 04/17] test: add FroidurePin binding-surface and integration tests --- test/runtests.jl | 1 + test/test_froidure_pin_temp.jl | 481 +++++++++++++++++++++++++++++++++ 2 files changed, 482 insertions(+) create mode 100644 test/test_froidure_pin_temp.jl diff --git a/test/runtests.jl b/test/runtests.jl index 9682068..79c8771 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_temp.jl") end diff --git a/test/test_froidure_pin_temp.jl b/test/test_froidure_pin_temp.jl new file mode 100644 index 0000000..a85aae4 --- /dev/null +++ b/test/test_froidure_pin_temp.jl @@ -0,0 +1,481 @@ +# 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_temp.jl - FroidurePin binding-surface and integration tests + +Section 1: Binding-surface tests against LibSemigroups.* directly. + These should PASS because the C++ bindings are already wired up. + +Section 2: High-level integration tests against FroidurePin{E}. + These FAIL in the RED phase — the Julia wrapper doesn't exist yet. +""" + +# ============================================================================ +# Section 1: Binding-surface tests +# ============================================================================ + +@testset "FroidurePin binding surface" begin + + LS = Semigroups.LibSemigroups + FPB = LS.FroidurePinBase + + # ----------------------------------------------------------------------- + # FroidurePinBase — existence of the type + # ----------------------------------------------------------------------- + @testset "FroidurePinBase type exists" begin + @test FPB isa DataType || FPB isa UnionAll + end + + # ----------------------------------------------------------------------- + # FroidurePinBase — methods dispatching on the base type + # ----------------------------------------------------------------------- + @testset "FroidurePinBase methods" begin + # Size / enumeration + @test hasmethod(LS.size, Tuple{FPB}) + @test hasmethod(LS.current_size, Tuple{FPB}) + @test hasmethod(LS.degree, Tuple{FPB}) + @test hasmethod(LS.number_of_generators, Tuple{FPB}) + + # Settings + @test hasmethod(LS.batch_size, Tuple{FPB}) + @test hasmethod(LS.set_batch_size!, Tuple{FPB, UInt}) + + # Enumeration control + @test hasmethod(LS.enumerate!, Tuple{FPB, UInt}) + + # Rules + @test hasmethod(LS.number_of_rules, Tuple{FPB}) + @test hasmethod(LS.current_number_of_rules, Tuple{FPB}) + @test hasmethod(LS.current_max_word_length, Tuple{FPB}) + + # Identity checks + @test hasmethod(LS.contains_one, Tuple{FPB}) + @test hasmethod(LS.currently_contains_one, Tuple{FPB}) + + # Length distribution + @test hasmethod(LS.number_of_elements_of_length, Tuple{FPB, UInt}) + @test hasmethod(LS.number_of_elements_of_length_range, Tuple{FPB, UInt, UInt}) + + # Index queries — checked variants + @test hasmethod(LS.prefix, Tuple{FPB, UInt32}) + @test hasmethod(LS.suffix, Tuple{FPB, UInt32}) + @test hasmethod(LS.first_letter, Tuple{FPB, UInt32}) + @test hasmethod(LS.final_letter, Tuple{FPB, UInt32}) + @test hasmethod(LS.current_length, Tuple{FPB, UInt32}) + @test hasmethod(LS.length, Tuple{FPB, UInt32}) + @test hasmethod(LS.position_of_generator, Tuple{FPB, UInt32}) + + # Index queries — _no_checks variants + @test hasmethod(LS.prefix_no_checks, Tuple{FPB, UInt32}) + @test hasmethod(LS.suffix_no_checks, Tuple{FPB, UInt32}) + @test hasmethod(LS.first_letter_no_checks, Tuple{FPB, UInt32}) + @test hasmethod(LS.final_letter_no_checks, Tuple{FPB, UInt32}) + @test hasmethod(LS.current_length_no_checks, Tuple{FPB, UInt32}) + @test hasmethod(LS.length_no_checks, Tuple{FPB, UInt32}) + @test hasmethod(LS.position_of_generator_no_checks, Tuple{FPB, UInt32}) + + # Cayley graphs + @test hasmethod(LS.right_cayley_graph, Tuple{FPB}) + @test hasmethod(LS.current_right_cayley_graph, Tuple{FPB}) + @test hasmethod(LS.left_cayley_graph, Tuple{FPB}) + @test hasmethod(LS.current_left_cayley_graph, Tuple{FPB}) + end + + # ----------------------------------------------------------------------- + # Module-level froidure_pin:: free functions (bound on FPB) + # ----------------------------------------------------------------------- + @testset "froidure_pin free functions" begin + @test hasmethod(LS.current_minimal_factorisation, Tuple{FPB, UInt32}) + @test hasmethod(LS.current_minimal_factorisation_no_checks, Tuple{FPB, UInt32}) + @test hasmethod(LS.minimal_factorisation, Tuple{FPB, UInt32}) + @test hasmethod(LS.factorisation, Tuple{FPB, UInt32}) + + # Word-position queries (ArrayRef variants detected at runtime) + @test isdefined(LS, :current_position) + @test isdefined(LS, :position) + @test isdefined(LS, :product_by_reduction) + @test isdefined(LS, :product_by_reduction_no_checks) + + # Rules / normal forms + @test isdefined(LS, :rules_lhs) + @test isdefined(LS, :rules_rhs) + @test isdefined(LS, :current_rules_lhs) + @test isdefined(LS, :current_rules_rhs) + @test isdefined(LS, :normal_forms) + @test isdefined(LS, :current_normal_forms) + end + + # ----------------------------------------------------------------------- + # FroidurePinTransf1 — representative element-typed type + # ----------------------------------------------------------------------- + @testset "FroidurePinTransf1 type exists" begin + @test isdefined(LS, :FroidurePinTransf1) + FPT1 = LS.FroidurePinTransf1 + @test FPT1 isa DataType || FPT1 isa UnionAll + end + + @testset "FroidurePinTransf1 element-typed methods" begin + FPT1 = LS.FroidurePinTransf1 + T1 = LS.Transf1 + + # Element access + @test hasmethod(LS.at, Tuple{FPT1, UInt}) + @test hasmethod(LS.sorted_at, Tuple{FPT1, UInt}) + @test hasmethod(LS.sorted_at_no_checks, Tuple{FPT1, UInt}) + @test hasmethod(LS.generator, Tuple{FPT1, UInt}) + @test hasmethod(LS.generator_no_checks, Tuple{FPT1, UInt}) + + # Containment / position + @test hasmethod(LS.contains, Tuple{FPT1, T1}) + @test hasmethod(LS.position, Tuple{FPT1, T1}) + @test hasmethod(LS.current_position, Tuple{FPT1, T1}) + @test hasmethod(LS.sorted_position, Tuple{FPT1, T1}) + @test hasmethod(LS.to_sorted_position, Tuple{FPT1, UInt}) + + # Fast product + @test hasmethod(LS.fast_product, Tuple{FPT1, UInt, UInt}) + @test hasmethod(LS.fast_product_no_checks, Tuple{FPT1, UInt, UInt}) + + # Idempotents + @test hasmethod(LS.number_of_idempotents, Tuple{FPT1}) + @test hasmethod(LS.is_idempotent, Tuple{FPT1, UInt}) + @test hasmethod(LS.is_idempotent_no_checks, Tuple{FPT1, UInt}) + + # Modification + @test hasmethod(LS.add_generator!, Tuple{FPT1, T1}) + @test hasmethod(LS.add_generator_no_checks!, Tuple{FPT1, T1}) + @test hasmethod(LS.closure!, Tuple{FPT1, T1}) + @test hasmethod(LS.reserve!, Tuple{FPT1, UInt}) + + # Display + @test isdefined(LS, :to_human_readable_repr) + + # Materialized collections + @test isdefined(LS, :idempotents) + @test isdefined(LS, :sorted_elements) + + # Word-element conversion + @test isdefined(LS, :to_element) + @test isdefined(LS, :to_element_no_checks) + @test isdefined(LS, :equal_to) + @test isdefined(LS, :equal_to_no_checks) + end + + # ----------------------------------------------------------------------- + # All 10 FroidurePin concrete types exist + # ----------------------------------------------------------------------- + @testset "All FroidurePin concrete types defined" begin + for sym in (:FroidurePinTransf1, :FroidurePinTransf2, :FroidurePinTransf4, + :FroidurePinPPerm1, :FroidurePinPPerm2, :FroidurePinPPerm4, + :FroidurePinPerm1, :FroidurePinPerm2, :FroidurePinPerm4, + :FroidurePinBMat8) + @test isdefined(LS, sym) + end + end + + # ----------------------------------------------------------------------- + # Smoke-test: construct and run a real FroidurePin via LibSemigroups.* + # directly (no high-level wrapper). This exercises the constructor + # lambdas and the size method across the C++ boundary. + # ----------------------------------------------------------------------- + @testset "FroidurePinTransf1 smoke test (LibSemigroups.* direct)" begin + # S₃ — symmetric group on 3 letters. + # Use Transf (high-level wrapper) to build the generators, then extract + # the raw C++ object (.cxx_obj) to pass to the CxxWrap constructor lambda. + # Generators in 1-based Julia: (1 2) → [2,1,3], (1 2 3) → [2,3,1]. + g1 = Transf([2, 1, 3]).cxx_obj # ::Transf1 (the CxxWrap type) + g2 = Transf([2, 3, 1]).cxx_obj + + fp = LS.FroidurePinTransf1(g1, g2) + @test LS.size(fp) == 6 + @test LS.number_of_generators(fp) == 2 + @test LS.degree(fp) == 3 + end + +end # @testset "FroidurePin binding surface" + + +# ============================================================================ +# Section 2: High-level integration tests (RED — wrapper doesn't exist yet) +# ============================================================================ + +# --------------------------------------------------------------------------- +# Parametric helper: basic size/collect contract +# --------------------------------------------------------------------------- +function check_fp_basic(gens, expected_size) + S = FroidurePin(gens...) + @test length(S) == expected_size + @test number_of_generators(S) == length(gens) + elts = collect(S) + @test length(elts) == expected_size + @test length(unique(elts)) == expected_size +end + +@testset "FroidurePin{E} high-level API" begin + + # ----------------------------------------------------------------------- + # Construction: type dispatch + # ----------------------------------------------------------------------- + @testset "Construction and type" begin + # S₃ from Transf generators + g1 = Transf([2, 1, 3]) # (1 2) in 1-based + g2 = Transf([2, 3, 1]) # (1 2 3) in 1-based + S = FroidurePin(g1, g2) + @test S isa FroidurePin{Transf{UInt8}} + + # PPerm generators + p1 = PPerm([2, 1, 3]) + p2 = PPerm([1, 3, 2]) + Sp = FroidurePin(p1, p2) + @test Sp isa FroidurePin{PPerm{UInt8}} + + # Perm generators + q1 = Perm([2, 1, 3]) + q2 = Perm([2, 3, 1]) + Sq = FroidurePin(q1, q2) + @test Sq isa FroidurePin{Perm{UInt8}} + + # BMat8 generators (2×2 blocks) + b1 = BMat8([[0, 1], [1, 0]]) + b2 = BMat8([[1, 0], [1, 1]]) + Sb = FroidurePin(b1, b2) + @test Sb isa FroidurePin{BMat8} + end + + # ----------------------------------------------------------------------- + # S₃ — primary test case throughout + # ----------------------------------------------------------------------- + @testset "S₃ — length and size" begin + S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) + @test length(S) == 6 + end + + @testset "S₃ — check_fp_basic parametric helper" begin + check_fp_basic([Transf([2, 1, 3]), Transf([2, 3, 1])], 6) + end + + @testset "S₃ — iteration / collect" begin + S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) + elts = collect(S) + @test elts isa Vector{Transf{UInt8}} + @test length(elts) == 6 + @test length(unique(elts)) == 6 + end + + @testset "S₃ — getindex (1-based)" begin + S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) + # Valid access + @test S[1] isa Transf{UInt8} + @test S[6] isa Transf{UInt8} + # Out-of-bounds (0-based is invalid) + @test_throws BoundsError S[0] + @test_throws BoundsError S[7] + end + + @testset "S₃ — in / contains" begin + S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) + @test Transf([2, 1, 3]) in S + @test Transf([1, 2, 3]) in S # identity is in S₃ + @test !(Transf([1, 1, 1]) in S) + end + + @testset "S₃ — push! adds a generator" begin + S = FroidurePin(Transf([2, 1, 3])) + n_before = number_of_generators(S) + push!(S, Transf([2, 3, 1])) + @test number_of_generators(S) == n_before + 1 + # After adding the second generator the semigroup is S₃ + @test length(S) == 6 + end + + @testset "S₃ — rules" begin + S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) + rs = rules(S) + @test rs isa Vector{Pair{Vector{Int}, Vector{Int}}} + # There must be at least one rule (|S₃| < |free monoid|) + @test length(rs) > 0 + # Each lhs and rhs must be non-empty 1-based generator-index vectors + for (lhs, rhs) in rs + @test all(x -> 1 <= x <= 2, lhs) + @test all(x -> 1 <= x <= 2, rhs) + end + end + + @testset "S₃ — minimal_factorisation (1-based)" begin + S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) + # Position 1 is always a generator — its factorisation is a length-1 word + w = minimal_factorisation(S, 1) + @test w isa Vector{Int} + @test length(w) == 1 + @test 1 <= w[1] <= 2 + # Out-of-bounds position + @test_throws Exception minimal_factorisation(S, 0) + @test_throws Exception minimal_factorisation(S, 7) + end + + @testset "S₃ — Cayley graphs" begin + S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) + rcg = right_cayley_graph(S) + lcg = left_cayley_graph(S) + # Should return something non-nothing (WordGraph or similar) + @test rcg !== nothing + @test lcg !== nothing + end + + @testset "S₃ — Runner interface" begin + S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) + @test !finished(S) # not yet run + run!(S) + @test finished(S) + end + + @testset "S₃ — idempotents" begin + S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) + ids = idempotents(S) + @test ids isa Vector{Transf{UInt8}} + # Every element in `ids` must satisfy x*x == x + for x in ids + @test x * x == x + end + # S₃ has exactly 1 idempotent (the identity) + @test length(ids) == 1 + end + + @testset "S₃ — sorted_elements" begin + S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) + se = sorted_elements(S) + @test se isa Vector{Transf{UInt8}} + @test length(se) == 6 + # Sorted means weakly increasing + for i in 2:length(se) + @test !(se[i] < se[i-1]) + end + end + + @testset "S₃ — number_of_generators" begin + S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) + @test number_of_generators(S) == 2 + end + + # ----------------------------------------------------------------------- + # Multiple element types: PPerm + # ----------------------------------------------------------------------- + @testset "PPerm generators" begin + # Symmetric inverse monoid on 2 points: pperm([2,1]) and pperm([], [], 2) + p1 = PPerm([2, 1]) + p2 = PPerm(Int[], Int[], 2) + S = FroidurePin(p1, p2) + @test S isa FroidurePin{PPerm{UInt8}} + elts = collect(S) + @test length(elts) > 0 + for x in elts + @test x isa PPerm{UInt8} + end + end + + # ----------------------------------------------------------------------- + # Multiple element types: Perm + # ----------------------------------------------------------------------- + @testset "Perm generators — S₃" begin + q1 = Perm([2, 1, 3]) + q2 = Perm([2, 3, 1]) + S = FroidurePin(q1, q2) + @test S isa FroidurePin{Perm{UInt8}} + @test length(S) == 6 + elts = collect(S) + @test all(x -> x isa Perm{UInt8}, elts) + end + + # ----------------------------------------------------------------------- + # Multiple element types: BMat8 + # ----------------------------------------------------------------------- + @testset "BMat8 generators" begin + # Small semigroup: 2 boolean 2×2 matrices + b1 = BMat8([[0, 1], [1, 0]]) + b2 = BMat8([[1, 0], [1, 1]]) + S = FroidurePin(b1, b2) + @test S isa FroidurePin{BMat8} + @test length(S) > 0 + elts = collect(S) + @test all(x -> x isa BMat8, elts) + end + + # ----------------------------------------------------------------------- + # Larger example: 5-generator transformation semigroup of degree 6 + # Reference: U from test-froidure-pin-transf.cpp test 049 → size 7776 + # ----------------------------------------------------------------------- + @testset "5-generator Transf degree-6 semigroup" begin + gens = [ + 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]), + ] + check_fp_basic(gens, 7776) + end + + # ----------------------------------------------------------------------- + # current_size: before full enumeration + # ----------------------------------------------------------------------- + @testset "current_size before enumerate" begin + S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) + # Without enumerating, current_size == number of generators + @test current_size(S) == 2 + # After enumerate!, current_size == full size + enumerate!(S, 100) + @test current_size(S) == 6 + end + + # ----------------------------------------------------------------------- + # degree + # ----------------------------------------------------------------------- + @testset "degree" begin + S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) + @test degree(S) == 3 + end + + # ----------------------------------------------------------------------- + # fast_product + # ----------------------------------------------------------------------- + @testset "fast_product (0-based internally, 1-based Julia)" begin + S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) + # Ensure the semigroup is fully enumerated + run!(S) + # product of element 1 and element 2 (1-based) must be in [1, 6] + idx = fast_product(S, 1, 2) + @test 1 <= idx <= 6 + end + + # ----------------------------------------------------------------------- + # is_idempotent (1-based position) + # ----------------------------------------------------------------------- + @testset "is_idempotent (1-based)" begin + S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) + run!(S) + for i in 1:length(S) + x = S[i] + @test is_idempotent(S, i) == (x * x == x) + end + end + + # ----------------------------------------------------------------------- + # to_element: word → element round-trip + # ----------------------------------------------------------------------- + @testset "to_element word round-trip" begin + S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) + run!(S) + # Generator 1 (1-based) is S[1] or S[position_of_generator(S,1)] + w = [1] # word consisting of first generator + x = to_element(S, w) + @test x isa Transf{UInt8} + # Factorising x should give back a word whose product = x + wf = minimal_factorisation(S, position(S, x)) + @test to_element(S, wf) == x + end + +end # @testset "FroidurePin{E} high-level API" From fe4d60974fcd9c70d239f5e1e9a47c60b63db3c1 Mon Sep 17 00:00:00 2001 From: James Swent Date: Wed, 22 Apr 2026 23:36:38 +0100 Subject: [PATCH 05/17] feat: FroidurePin{E} constructors and size queries --- src/Semigroups.jl | 6 + src/froidure-pin.jl | 290 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 296 insertions(+) create mode 100644 src/froidure-pin.jl diff --git a/src/Semigroups.jl b/src/Semigroups.jl index f69ebba..188768a 100644 --- a/src/Semigroups.jl +++ b/src/Semigroups.jl @@ -74,6 +74,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 +163,9 @@ 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! + # 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..8e7030e --- /dev/null +++ b/src/froidure-pin.jl @@ -0,0 +1,290 @@ +# 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 + +""" + _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} + +A Froidure-Pin semigroup over elements of type `E`. + +The Froidure-Pin algorithm is used to enumerate all elements of a finitely +generated semigroup. This type wraps the C++ `FroidurePin` class from +libsemigroups. + +Supported element types: +- `Transf{UInt8}`, `Transf{UInt16}`, `Transf{UInt32}` (transformations) +- `PPerm{UInt8}`, `PPerm{UInt16}`, `PPerm{UInt32}` (partial permutations) +- `Perm{UInt8}`, `Perm{UInt16}`, `Perm{UInt32}` (permutations) +- `BMat8` (boolean matrices up to 8x8) + +# Example +```julia +using Semigroups + +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +length(S) # 6 (S_3) +``` +""" +mutable struct FroidurePin{E} + cxx_obj::_FroidurePinCxx +end + +# ============================================================================ +# Constructors +# ============================================================================ + +""" + FroidurePin(gens::Vector{E}) where {E} + +Construct a `FroidurePin{E}` from a vector of generators. + +# Example +```julia +using Semigroups + +S = FroidurePin([Transf([2, 1, 3]), Transf([2, 3, 1])]) +length(S) # 6 +``` +""" +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 in 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}` from one or more generators (variadic). + +# Example +```julia +using Semigroups + +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +length(S) # 6 +``` +""" +function FroidurePin(x::E, xs::E...) where {E} + return FroidurePin(E[x, xs...]) +end + +# ============================================================================ +# Size queries +# ============================================================================ + +""" + Base.length(fp::FroidurePin) -> Int + +Return the total number of elements in the semigroup. + +Triggers full enumeration if not already complete. + +# Example +```julia +using Semigroups + +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +length(S) # 6 +``` +""" +Base.length(fp::FroidurePin) = Int(LibSemigroups.size(fp.cxx_obj)) + +""" + current_size(fp::FroidurePin) -> Int + +Return the number of elements enumerated so far (without triggering +further enumeration). + +# Example +```julia +using Semigroups + +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +current_size(S) # 2 (only generators known) +``` +""" +current_size(fp::FroidurePin) = Int(LibSemigroups.current_size(fp.cxx_obj)) + +""" + degree(fp::FroidurePin) -> Int + +Return the degree of the elements in the semigroup. + +# 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 of the semigroup. + +# Example +```julia +using Semigroups + +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +number_of_generators(S) # 2 +``` +""" +number_of_generators(fp::FroidurePin) = Int(LibSemigroups.number_of_generators(fp.cxx_obj)) + +""" + enumerate!(fp::FroidurePin, limit::Integer) + +Enumerate elements until at least `limit` elements have been found. + +This partially enumerates the semigroup. After calling this, `current_size` +will be at least `min(limit, length(fp))`. +""" +function enumerate!(fp::FroidurePin, limit::Integer) + @wrap_libsemigroups_call LibSemigroups.enumerate!(fp.cxx_obj, UInt(limit)) + return fp +end From 65f53d11de660b59211285ad88f730c90e3236bc Mon Sep 17 00:00:00 2001 From: James Swent Date: Thu, 23 Apr 2026 00:35:14 +0100 Subject: [PATCH 06/17] feat: FroidurePin Runner delegation, element access, iteration --- src/Semigroups.jl | 2 + src/froidure-pin.jl | 305 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 307 insertions(+) diff --git a/src/Semigroups.jl b/src/Semigroups.jl index 188768a..493701e 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 @@ -165,6 +166,7 @@ export left_one, right_one # FroidurePin export FroidurePin, current_size, number_of_generators, enumerate! +export generator, sorted_at # BMat8 export BMat8, to_int, swap!, degree, random, row_space_basis diff --git a/src/froidure-pin.jl b/src/froidure-pin.jl index 8e7030e..2186030 100644 --- a/src/froidure-pin.jl +++ b/src/froidure-pin.jl @@ -288,3 +288,308 @@ function enumerate!(fp::FroidurePin, limit::Integer) @wrap_libsemigroups_call LibSemigroups.enumerate!(fp.cxx_obj, UInt(limit)) 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). + +Returns `fp` for method chaining. +""" +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. + +Returns `fp` for method chaining. + +# Examples +```julia +run_for!(S, Second(1)) +run_for!(S, Millisecond(500)) +``` +""" +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). + +Returns `fp` for method chaining. + +Supports do-block syntax: + +```julia +run_until!(S) do + some_condition(S) +end +``` +""" +run_until!(fp::FroidurePin, f::Function) = run_until!(f, fp) + +""" + init!(fp::FroidurePin) -> FroidurePin + +Initialize an existing FroidurePin object, resetting it to its default state. + +Returns `fp` for method chaining. +""" +function init!(fp::FroidurePin) + LibSemigroups.init!(fp.cxx_obj) + return fp +end + +""" + kill!(fp::FroidurePin) + +Stop the Froidure-Pin algorithm from running (thread-safe). +""" +kill!(fp::FroidurePin) = LibSemigroups.kill!(fp.cxx_obj) + +""" + finished(fp::FroidurePin) -> Bool + +Check if the Froidure-Pin algorithm has been run to completion. +""" +finished(fp::FroidurePin) = LibSemigroups.finished(fp.cxx_obj) + +""" + Base.success(fp::FroidurePin) -> Bool + +Check if the Froidure-Pin algorithm has been run to completion successfully. +""" +Base.success(fp::FroidurePin) = LibSemigroups.success(fp.cxx_obj) + +""" + started(fp::FroidurePin) -> Bool + +Check if the Froidure-Pin algorithm has been started. +""" +started(fp::FroidurePin) = LibSemigroups.started(fp.cxx_obj) + +""" + running(fp::FroidurePin) -> Bool + +Check if the Froidure-Pin algorithm is currently running. +""" +running(fp::FroidurePin) = LibSemigroups.running(fp.cxx_obj) + +""" + timed_out(fp::FroidurePin) -> Bool + +Check if the last `run_for!` call timed out. +""" +timed_out(fp::FroidurePin) = LibSemigroups.timed_out(fp.cxx_obj) + +""" + stopped(fp::FroidurePin) -> Bool + +Check if the Froidure-Pin algorithm is stopped for any reason. +""" +stopped(fp::FroidurePin) = LibSemigroups.stopped(fp.cxx_obj) + +""" + dead(fp::FroidurePin) -> Bool + +Check if the Froidure-Pin algorithm has been killed by another thread. +""" +dead(fp::FroidurePin) = LibSemigroups.dead(fp.cxx_obj) + +""" + stopped_by_predicate(fp::FroidurePin) -> Bool + +Check if the algorithm was stopped by the predicate passed to `run_until!`. +""" +stopped_by_predicate(fp::FroidurePin) = LibSemigroups.stopped_by_predicate(fp.cxx_obj) + +""" + running_for(fp::FroidurePin) -> Bool + +Check if the algorithm is currently running for a particular length of time. +""" +running_for(fp::FroidurePin) = LibSemigroups.running_for(fp.cxx_obj) + +""" + running_for_how_long(fp::FroidurePin) -> Nanosecond + +Return the duration of the most recent `run_for!` call as a `Dates.Nanosecond`. +""" +running_for_how_long(fp::FroidurePin) = Nanosecond(LibSemigroups.running_for_how_long(fp.cxx_obj)) + +""" + running_until(fp::FroidurePin) -> Bool + +Check if the algorithm is currently running until a predicate returns `true`. +""" +running_until(fp::FroidurePin) = LibSemigroups.running_until(fp.cxx_obj) + +""" + current_state(fp::FroidurePin) -> RunnerState + +Return the current state of the Froidure-Pin algorithm. +""" +current_state(fp::FroidurePin) = LibSemigroups.current_state(fp.cxx_obj) + +""" + report_why_we_stopped(fp::FroidurePin) + +Report why the Froidure-Pin algorithm stopped. +""" +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 the algorithm stopped. +""" +string_why_we_stopped(fp::FroidurePin) = LibSemigroups.string_why_we_stopped(fp.cxx_obj) + +# ============================================================================ +# Element access +# ============================================================================ + +""" + Base.getindex(fp::FroidurePin{E}, i::Integer) -> E + +Return the `i`-th element of the semigroup (1-based indexing). + +Triggers full enumeration if not already complete. + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +S[1] # first element +``` + +# Throws +- `BoundsError` if `i` is out of range. +""" +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 `i`-th generator of the semigroup (1-based indexing). + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +generator(S, 1) # first generator +``` +""" +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 + +Return the `i`-th element in sorted order (1-based indexing). + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +sorted_at(S, 1) # first element in sorted order +``` +""" +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, state=1) + +Iterate over all elements of the semigroup. + +# 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 +``` +""" +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 + +Return the element type `E` of a `FroidurePin{E}`. +""" +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 semigroup by reconstructing it from +its generators. +""" +function Base.copy(fp::FroidurePin{E}) where {E} + gens = [generator(fp, i) for i in 1:number_of_generators(fp)] + return FroidurePin(gens) +end + +# ============================================================================ +# Display +# ============================================================================ + +""" + Base.show(io::IO, fp::FroidurePin) + +Display a human-readable representation of the semigroup. +""" +function Base.show(io::IO, fp::FroidurePin) + print(io, @wrap_libsemigroups_call LibSemigroups.to_human_readable_repr(fp.cxx_obj)) +end From 9898d93f23e905439be261fd9525e781be12368a Mon Sep 17 00:00:00 2001 From: James Swent Date: Thu, 23 Apr 2026 00:46:36 +0100 Subject: [PATCH 07/17] feat: FroidurePin containment, modification, index queries Add containment (Base.in, position, sorted_position, to_sorted_position), modification (Base.push!, closure!, copy_closure, copy_add_generators), settings (batch_size, set_batch_size!), predicates (contains_one, is_idempotent), and index queries (prefix, suffix, first_letter, final_letter, fast_product, number_of_rules, number_of_idempotents) to the FroidurePin{E} high-level wrapper. --- src/Semigroups.jl | 6 + src/froidure-pin.jl | 308 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 314 insertions(+) diff --git a/src/Semigroups.jl b/src/Semigroups.jl index 493701e..4eb8e18 100644 --- a/src/Semigroups.jl +++ b/src/Semigroups.jl @@ -167,6 +167,12 @@ export left_one, right_one # FroidurePin export FroidurePin, current_size, number_of_generators, enumerate! export generator, sorted_at +export position, sorted_position, to_sorted_position +export closure!, copy_closure, copy_add_generators +export batch_size, set_batch_size! +export contains_one, is_idempotent +export prefix, suffix, first_letter, final_letter, fast_product +export number_of_idempotents # BMat8 export BMat8, to_int, swap!, degree, random, row_space_basis diff --git a/src/froidure-pin.jl b/src/froidure-pin.jl index 2186030..b1ed79b 100644 --- a/src/froidure-pin.jl +++ b/src/froidure-pin.jl @@ -593,3 +593,311 @@ Display a human-readable representation of the semigroup. 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 + +Check whether the element `x` belongs to the semigroup `fp`. + +Triggers full enumeration if not already complete. + +# 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 +``` +""" +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 -> Int + +Return the 1-based position of element `x` in the semigroup `fp`. + +Triggers full enumeration if not already complete. + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +position(S, Transf([2, 1, 3])) # 1-based index +``` +""" +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 -> Int + +Return the 1-based sorted position of element `x` in the semigroup `fp`. + +Triggers full enumeration if not already complete. +""" +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) -> Int + +Convert a 1-based element position to its 1-based sorted position. + +Triggers full enumeration if not already complete. +""" +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 + +# ============================================================================ +# 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 + +# ============================================================================ +# Settings +# ============================================================================ + +""" + batch_size(fp::FroidurePin) -> Int + +Return the current batch size used for partial enumeration. +""" +batch_size(fp::FroidurePin) = Int(LibSemigroups.batch_size(fp.cxx_obj)) + +""" + set_batch_size!(fp::FroidurePin, n::Integer) -> FroidurePin + +Set the batch size for partial enumeration. + +Returns `fp` for method chaining. +""" +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 whether the semigroup contains the identity element. + +Triggers full enumeration if not already complete. +""" +contains_one(fp::FroidurePin) = LibSemigroups.contains_one(fp.cxx_obj) + +""" + is_idempotent(fp::FroidurePin, i::Integer) -> Bool + +Check whether the element at 1-based position `i` is an idempotent +(i.e., `x * x == x`). + +Triggers full enumeration if not already complete. +""" +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 1-based position of the prefix of the element at 1-based +position `i`. The prefix is the element obtained by removing the last +letter of the factorisation. +""" +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 1-based position of the suffix of the element at 1-based +position `i`. The suffix is the element obtained by removing the first +letter of the factorisation. +""" +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 1-based position of the first letter (generator) in the +factorisation of the element at 1-based position `i`. +""" +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 1-based position of the final letter (generator) in the +factorisation of the element at 1-based position `i`. +""" +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)) + +""" + 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)) From a21eeb6e880c06483d3c00d0ed1e1a6ca6f21b48 Mon Sep 17 00:00:00 2001 From: James Swent Date: Thu, 23 Apr 2026 09:02:49 +0100 Subject: [PATCH 08/17] feat: FroidurePin collections, factorisations, Cayley graphs, word ops Add high-level Julia wrappers for the remaining FroidurePin methods: - rules/current_rules (Pair-based), normal_forms/current_normal_forms - idempotents, sorted_elements (with _copy_cxx_element for StdVector safety) - minimal_factorisation, current_minimal_factorisation, factorisation - position(fp, word), right/left_cayley_graph, to_element, equal_to --- src/Semigroups.jl | 6 + src/froidure-pin.jl | 277 +++++++++++++++++++++++++++++++++ test/test_froidure_pin_temp.jl | 2 +- 3 files changed, 284 insertions(+), 1 deletion(-) diff --git a/src/Semigroups.jl b/src/Semigroups.jl index 4eb8e18..5eddf39 100644 --- a/src/Semigroups.jl +++ b/src/Semigroups.jl @@ -173,6 +173,12 @@ export batch_size, set_batch_size! export contains_one, is_idempotent export prefix, suffix, first_letter, final_letter, fast_product export number_of_idempotents +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 diff --git a/src/froidure-pin.jl b/src/froidure-pin.jl index b1ed79b..17ce063 100644 --- a/src/froidure-pin.jl +++ b/src/froidure-pin.jl @@ -100,6 +100,19 @@ _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 @@ -901,3 +914,267 @@ 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)) + +# ============================================================================ +# 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 in 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 in 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/test/test_froidure_pin_temp.jl b/test/test_froidure_pin_temp.jl index a85aae4..cb2b9f6 100644 --- a/test/test_froidure_pin_temp.jl +++ b/test/test_froidure_pin_temp.jl @@ -474,7 +474,7 @@ end x = to_element(S, w) @test x isa Transf{UInt8} # Factorising x should give back a word whose product = x - wf = minimal_factorisation(S, position(S, x)) + wf = minimal_factorisation(S, Semigroups.position(S, x)) @test to_element(S, wf) == x end From 9538953dfc9085076ab0b9a26c19d368f2d45ce5 Mon Sep 17 00:00:00 2001 From: James Swent Date: Thu, 23 Apr 2026 23:24:00 +0100 Subject: [PATCH 09/17] feat: add missing FroidurePin Julia wrappers Add Julia-level wrappers for C++-bound methods that were missed: - product_by_reduction: product via Cayley graph indices - position_of_generator: position of i-th generator - current_length / word_length: factorisation length - currently_contains_one: no-enumeration identity check - current_number_of_rules: no-enumeration rule count - current_max_word_length: no-enumeration max word length - number_of_elements_of_length: count by exact/range length --- src/Semigroups.jl | 7 +++- src/froidure-pin.jl | 99 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/src/Semigroups.jl b/src/Semigroups.jl index 5eddf39..4359fef 100644 --- a/src/Semigroups.jl +++ b/src/Semigroups.jl @@ -170,9 +170,12 @@ export generator, sorted_at export position, sorted_position, to_sorted_position export closure!, copy_closure, copy_add_generators export batch_size, set_batch_size! -export contains_one, is_idempotent +export contains_one, currently_contains_one, is_idempotent export prefix, suffix, first_letter, final_letter, fast_product -export number_of_idempotents +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 diff --git a/src/froidure-pin.jl b/src/froidure-pin.jl index 17ce063..ddc010d 100644 --- a/src/froidure-pin.jl +++ b/src/froidure-pin.jl @@ -906,6 +906,14 @@ 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 @@ -915,6 +923,97 @@ 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 # ============================================================================ From e9d683e39cba9cd01d177aa88fb36b318bab66de Mon Sep 17 00:00:00 2001 From: James Swent Date: Fri, 24 Apr 2026 10:07:05 +0100 Subject: [PATCH 10/17] feat: port test-froidure-pin-transf.cpp from libsemigroups --- src/Semigroups.jl | 3 +- src/froidure-pin.jl | 48 + src/transf.jl | 3 + test/runtests.jl | 1 + test/test_froidure_pin_transf.jl | 1473 ++++++++++++++++++++++++++++++ 5 files changed, 1527 insertions(+), 1 deletion(-) create mode 100644 test/test_froidure_pin_transf.jl diff --git a/src/Semigroups.jl b/src/Semigroups.jl index 4359fef..b6251be 100644 --- a/src/Semigroups.jl +++ b/src/Semigroups.jl @@ -168,8 +168,9 @@ export left_one, right_one export FroidurePin, current_size, number_of_generators, enumerate! export generator, sorted_at export position, sorted_position, to_sorted_position -export closure!, copy_closure, copy_add_generators +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 diff --git a/src/froidure-pin.jl b/src/froidure-pin.jl index ddc010d..0096d01 100644 --- a/src/froidure-pin.jl +++ b/src/froidure-pin.jl @@ -693,6 +693,41 @@ function to_sorted_position(fp::FroidurePin, i::Integer) return _from_cpp(raw) end +""" + current_position(fp::FroidurePin{E}, x::E) where E -> Int + +Return the 1-based position of element `x` among the elements +enumerated so far (without triggering further enumeration). + +Returns `UNDEFINED` if `x` has not yet been enumerated. +""" +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}) -> Int + +Return the 1-based position of the element represented by the 1-based +generator-index word `w` among the elements enumerated so far +(without triggering further enumeration). + +Returns `UNDEFINED` if the element has not yet been enumerated. +""" +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 # ============================================================================ @@ -779,6 +814,19 @@ function copy_add_generators(fp::FroidurePin{BMat8}, x::BMat8) 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) + @wrap_libsemigroups_call LibSemigroups.reserve!(fp.cxx_obj, UInt(n)) + return fp +end + # ============================================================================ # Settings # ============================================================================ 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/test/runtests.jl b/test/runtests.jl index 79c8771..81800fa 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -23,4 +23,5 @@ using Semigroups include("test_presentation_examples.jl") include("test_word_range.jl") include("test_froidure_pin_temp.jl") + include("test_froidure_pin_transf.jl") end diff --git a/test/test_froidure_pin_transf.jl b/test/test_froidure_pin_transf.jl new file mode 100644 index 0000000..118e208 --- /dev/null +++ b/test/test_froidure_pin_transf.jl @@ -0,0 +1,1473 @@ +# 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) From dcbbf64b02cec2a269321d2d39a90965a1cc2f0a Mon Sep 17 00:00:00 2001 From: James Swent Date: Fri, 24 Apr 2026 10:07:22 +0100 Subject: [PATCH 11/17] fix: add @cxxdereference to wg returns --- src/word-graph.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 From 58e96bf44f914f149bcc682db4143a59730f4387 Mon Sep 17 00:00:00 2001 From: James Swent Date: Fri, 24 Apr 2026 10:25:18 +0100 Subject: [PATCH 12/17] chore: run formatter --- test/test_froidure_pin_transf.jl | 2626 +++++++++++++++--------------- 1 file changed, 1316 insertions(+), 1310 deletions(-) diff --git a/test/test_froidure_pin_transf.jl b/test/test_froidure_pin_transf.jl index 118e208..e761c92 100644 --- a/test/test_froidure_pin_transf.jl +++ b/test/test_froidure_pin_transf.jl @@ -44,1430 +44,1436 @@ 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 + @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 - # ----------------------------------------------------------------------- - # 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 + # add_generators increases size + push!(S, Transf([8, 2, 3, 7, 8, 5, 2, 6])) + @test length(S) == 826713 - # ----------------------------------------------------------------------- - # 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 + # closure with already-present generator doesn't change size + closure!(S, Transf([8, 2, 3, 7, 8, 5, 2, 6])) + @test length(S) == 826713 - # ----------------------------------------------------------------------- - # 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 + # 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) - # ----------------------------------------------------------------------- - # 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 + # Every idempotent satisfies x * x == x + @test all(x * x == x for x in idempotents(S)) + @test length(idempotents(S)) == number_of_idempotents(S) - # ----------------------------------------------------------------------- - # 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) + # Sorted elements are strictly increasing + @test issorted(sorted_elements(S)) 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)) + # ----------------------------------------------------------------------- + # 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 - 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)) + # ----------------------------------------------------------------------- + # 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 - 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]), - ) + # ----------------------------------------------------------------------- + # 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 - for i = 1:length(U) - @test current_length(U, i) isa Integer - @test_throws Exception current_length(U, i + length(U)) + # ----------------------------------------------------------------------- + # 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 - 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) + # ----------------------------------------------------------------------- + # 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 - 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) + # ----------------------------------------------------------------------- + # 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 - 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 + # ----------------------------------------------------------------------- + # 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 position_of_generator(S, i + 1) + @test_throws Exception suffix(U, length(U) + 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 + # ----------------------------------------------------------------------- + # 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 - for i = 0:19 - @test_throws Exception is_idempotent(S, 442 + i) + + # ----------------------------------------------------------------------- + # 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 - 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])) + # ----------------------------------------------------------------------- + # 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 - # 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 + # ----------------------------------------------------------------------- + # 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 - # Adding generator with wrong degree throws - @test_throws LibsemigroupsError push!(T, Transf([2, 3, 2, 4, 2, 5, 2, 6, 2])) - 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 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 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 059: "small semigroup" [quick] - # ----------------------------------------------------------------------- - @testset "059: small semigroup" begin - S = FroidurePin(Transf([1, 2, 1]), Transf([1, 2, 3])) + # ----------------------------------------------------------------------- + # 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 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 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 S[1] == Transf([1, 2, 1]) - @test S[2] == Transf([1, 2, 3]) + # ----------------------------------------------------------------------- + # 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 Semigroups.position(S, Transf([1, 2, 1])) == 1 - @test Transf([1, 2, 1]) in S + # ----------------------------------------------------------------------- + # Test 059: "small semigroup" [quick] + # ----------------------------------------------------------------------- + @testset "059: small semigroup" begin + S = FroidurePin(Transf([1, 2, 1]), Transf([1, 2, 3])) - @test Semigroups.position(S, Transf([1, 2, 3])) == 2 - @test Transf([1, 2, 3]) in S + @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 Semigroups.position(S, Transf([1, 1, 1])) == UNDEFINED - @test !(Transf([1, 1, 1]) in S) - end + @test S[1] == Transf([1, 2, 1]) + @test S[2] == Transf([1, 2, 3]) - # ----------------------------------------------------------------------- - # 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 Semigroups.position(S, Transf([1, 2, 1])) == 1 + @test Transf([1, 2, 1]) in S - # ----------------------------------------------------------------------- - # 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 Semigroups.position(S, Transf([1, 2, 3])) == 2 + @test Transf([1, 2, 3]) in S - # ----------------------------------------------------------------------- - # 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 Semigroups.position(S, Transf([1, 1, 1])) == UNDEFINED + @test !(Transf([1, 1, 1]) in S) + 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 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 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 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 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 + # ----------------------------------------------------------------------- + # 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 - 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 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 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 + @test length(S) == 7776 + @test number_of_idempotents(S) == 537 + @test number_of_generators(S) == 5 + @test number_of_rules(S) == 2459 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 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 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 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 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 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 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 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 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 - ) + # ----------------------------------------------------------------------- + # 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 - ids = idempotents(S) - @test length(ids) == number_of_idempotents(S) - @test length(ids) == 6322 + # ----------------------------------------------------------------------- + # 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 - for x in ids - test_idempotent(S, x) + # ----------------------------------------------------------------------- + # 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 - # Second pass gives same count (repeatability) - ids2 = idempotents(S) - @test length(ids2) == 6322 - 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 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 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 - # ----------------------------------------------------------------------- - # 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]), - ) + # Second pass gives same count (repeatability) + ids2 = idempotents(S) + @test length(ids2) == 6322 + end - # Generators have sequential positions - for i = 1:5 - @test current_position(S, generator(S, i)) == i + # ----------------------------------------------------------------------- + # 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 - set_batch_size!(S, 1024) - enumerate!(S, 1024) - @test current_size(S) == 1029 + # ----------------------------------------------------------------------- + # 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 - # Element within enumerated range - @test current_position(S, Transf([6, 2, 6, 6, 3, 6])) == 1029 + set_batch_size!(S, 1024) + enumerate!(S, 1024) + @test current_size(S) == 1029 - # Wrong degree → UNDEFINED - @test current_position(S, Transf([6, 2, 6, 6, 3, 6, 7])) == UNDEFINED + # Element within enumerated range + @test current_position(S, Transf([6, 2, 6, 6, 3, 6])) == 1029 - # Not yet enumerated → UNDEFINED from current_position - @test current_position(S, Transf([6, 5, 6, 2, 1, 6])) == UNDEFINED + # Wrong degree → UNDEFINED + @test current_position(S, Transf([6, 2, 6, 6, 3, 6, 7])) == UNDEFINED - # But full position() finds it (triggers enumeration) - @test Semigroups.position(S, Transf([6, 5, 6, 2, 1, 6])) == 1030 - end + # Not yet enumerated → UNDEFINED from current_position + @test current_position(S, Transf([6, 5, 6, 2, 1, 6])) == UNDEFINED - # ----------------------------------------------------------------------- - # 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]), - ) + # But full position() finds it (triggers enumeration) + @test Semigroups.position(S, Transf([6, 5, 6, 2, 1, 6])) == 1030 + end - # 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 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]), + ) - @test sorted_position(S, generator(S, 2)) == 1391 - @test sorted_at(S, 1391) == generator(S, 2) + # 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, 3)) == 5236 - @test sorted_at(S, 5236) == generator(S, 3) + @test sorted_position(S, generator(S, 2)) == 1391 + @test sorted_at(S, 1391) == generator(S, 2) - @test sorted_position(S, generator(S, 4)) == 6791 - @test sorted_at(S, 6791) == generator(S, 4) + @test sorted_position(S, generator(S, 3)) == 5236 + @test sorted_at(S, 5236) == generator(S, 3) - @test sorted_position(S, generator(S, 5)) == 1607 - @test sorted_at(S, 1607) == generator(S, 5) + @test sorted_position(S, generator(S, 4)) == 6791 + @test sorted_at(S, 6791) == generator(S, 4) - @test finished(S) + @test sorted_position(S, generator(S, 5)) == 1607 + @test sorted_at(S, 1607) == generator(S, 5) - # Position-to-sorted conversion (C++ 1024 → Julia 1025) - @test to_sorted_position(S, 1025) == 6811 - @test sorted_at(S, 6811) == S[1025] + @test finished(S) - @test sorted_position(S, Transf([6, 2, 6, 6, 3, 6])) == 6909 - @test sorted_at(S, 6909) == Transf([6, 2, 6, 6, 3, 6]) + # Position-to-sorted conversion (C++ 1024 → Julia 1025) + @test to_sorted_position(S, 1025) == 6811 + @test sorted_at(S, 6811) == S[1025] - # Wrong degree → UNDEFINED - @test sorted_position(S, Transf([6, 6, 6, 2, 6, 6, 7])) == UNDEFINED + @test sorted_position(S, Transf([6, 2, 6, 6, 3, 6])) == 6909 + @test sorted_at(S, 6909) == Transf([6, 2, 6, 6, 3, 6]) - # Out of bounds - @test_throws Exception sorted_at(S, 100001) - @test_throws BoundsError S[100001] - @test to_sorted_position(S, 100001) == UNDEFINED - end + # Wrong degree → UNDEFINED + @test sorted_position(S, Transf([6, 6, 6, 2, 6, 6, 7])) == UNDEFINED - # ----------------------------------------------------------------------- - # 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 + # Out of bounds + @test_throws Exception sorted_at(S, 100001) + @test_throws BoundsError S[100001] + @test to_sorted_position(S, 100001) == UNDEFINED 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 + # ----------------------------------------------------------------------- + # 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 - 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) + # ----------------------------------------------------------------------- + # 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 - for (i, x) in Base.enumerate(se) - @test sorted_position(S, x) == i - @test to_sorted_position(S, Semigroups.position(S, x)) == i + # 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 - 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 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 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])) + # ----------------------------------------------------------------------- + # 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])) + 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 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 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 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 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 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 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 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 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 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 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 + # ----------------------------------------------------------------------- + # 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 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 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 + # ----------------------------------------------------------------------- + # 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 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 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 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 + # ----------------------------------------------------------------------- + # 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 + # 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 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 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 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 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 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 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 + # ----------------------------------------------------------------------- + # 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 - @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 + # ----------------------------------------------------------------------- + # 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 # @testset "FroidurePin" end # ReportGuard(false) From 503ade1f3ee88ef47d8c305e4961acec855e749e Mon Sep 17 00:00:00 2001 From: James Swent Date: Fri, 24 Apr 2026 10:31:17 +0100 Subject: [PATCH 13/17] chore: remove froidure pin temporary tests --- test/runtests.jl | 1 - test/test_froidure_pin_temp.jl | 481 --------------------------------- 2 files changed, 482 deletions(-) delete mode 100644 test/test_froidure_pin_temp.jl diff --git a/test/runtests.jl b/test/runtests.jl index 81800fa..214bd89 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -22,6 +22,5 @@ using Semigroups include("test_presentation.jl") include("test_presentation_examples.jl") include("test_word_range.jl") - include("test_froidure_pin_temp.jl") include("test_froidure_pin_transf.jl") end diff --git a/test/test_froidure_pin_temp.jl b/test/test_froidure_pin_temp.jl deleted file mode 100644 index cb2b9f6..0000000 --- a/test/test_froidure_pin_temp.jl +++ /dev/null @@ -1,481 +0,0 @@ -# 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_temp.jl - FroidurePin binding-surface and integration tests - -Section 1: Binding-surface tests against LibSemigroups.* directly. - These should PASS because the C++ bindings are already wired up. - -Section 2: High-level integration tests against FroidurePin{E}. - These FAIL in the RED phase — the Julia wrapper doesn't exist yet. -""" - -# ============================================================================ -# Section 1: Binding-surface tests -# ============================================================================ - -@testset "FroidurePin binding surface" begin - - LS = Semigroups.LibSemigroups - FPB = LS.FroidurePinBase - - # ----------------------------------------------------------------------- - # FroidurePinBase — existence of the type - # ----------------------------------------------------------------------- - @testset "FroidurePinBase type exists" begin - @test FPB isa DataType || FPB isa UnionAll - end - - # ----------------------------------------------------------------------- - # FroidurePinBase — methods dispatching on the base type - # ----------------------------------------------------------------------- - @testset "FroidurePinBase methods" begin - # Size / enumeration - @test hasmethod(LS.size, Tuple{FPB}) - @test hasmethod(LS.current_size, Tuple{FPB}) - @test hasmethod(LS.degree, Tuple{FPB}) - @test hasmethod(LS.number_of_generators, Tuple{FPB}) - - # Settings - @test hasmethod(LS.batch_size, Tuple{FPB}) - @test hasmethod(LS.set_batch_size!, Tuple{FPB, UInt}) - - # Enumeration control - @test hasmethod(LS.enumerate!, Tuple{FPB, UInt}) - - # Rules - @test hasmethod(LS.number_of_rules, Tuple{FPB}) - @test hasmethod(LS.current_number_of_rules, Tuple{FPB}) - @test hasmethod(LS.current_max_word_length, Tuple{FPB}) - - # Identity checks - @test hasmethod(LS.contains_one, Tuple{FPB}) - @test hasmethod(LS.currently_contains_one, Tuple{FPB}) - - # Length distribution - @test hasmethod(LS.number_of_elements_of_length, Tuple{FPB, UInt}) - @test hasmethod(LS.number_of_elements_of_length_range, Tuple{FPB, UInt, UInt}) - - # Index queries — checked variants - @test hasmethod(LS.prefix, Tuple{FPB, UInt32}) - @test hasmethod(LS.suffix, Tuple{FPB, UInt32}) - @test hasmethod(LS.first_letter, Tuple{FPB, UInt32}) - @test hasmethod(LS.final_letter, Tuple{FPB, UInt32}) - @test hasmethod(LS.current_length, Tuple{FPB, UInt32}) - @test hasmethod(LS.length, Tuple{FPB, UInt32}) - @test hasmethod(LS.position_of_generator, Tuple{FPB, UInt32}) - - # Index queries — _no_checks variants - @test hasmethod(LS.prefix_no_checks, Tuple{FPB, UInt32}) - @test hasmethod(LS.suffix_no_checks, Tuple{FPB, UInt32}) - @test hasmethod(LS.first_letter_no_checks, Tuple{FPB, UInt32}) - @test hasmethod(LS.final_letter_no_checks, Tuple{FPB, UInt32}) - @test hasmethod(LS.current_length_no_checks, Tuple{FPB, UInt32}) - @test hasmethod(LS.length_no_checks, Tuple{FPB, UInt32}) - @test hasmethod(LS.position_of_generator_no_checks, Tuple{FPB, UInt32}) - - # Cayley graphs - @test hasmethod(LS.right_cayley_graph, Tuple{FPB}) - @test hasmethod(LS.current_right_cayley_graph, Tuple{FPB}) - @test hasmethod(LS.left_cayley_graph, Tuple{FPB}) - @test hasmethod(LS.current_left_cayley_graph, Tuple{FPB}) - end - - # ----------------------------------------------------------------------- - # Module-level froidure_pin:: free functions (bound on FPB) - # ----------------------------------------------------------------------- - @testset "froidure_pin free functions" begin - @test hasmethod(LS.current_minimal_factorisation, Tuple{FPB, UInt32}) - @test hasmethod(LS.current_minimal_factorisation_no_checks, Tuple{FPB, UInt32}) - @test hasmethod(LS.minimal_factorisation, Tuple{FPB, UInt32}) - @test hasmethod(LS.factorisation, Tuple{FPB, UInt32}) - - # Word-position queries (ArrayRef variants detected at runtime) - @test isdefined(LS, :current_position) - @test isdefined(LS, :position) - @test isdefined(LS, :product_by_reduction) - @test isdefined(LS, :product_by_reduction_no_checks) - - # Rules / normal forms - @test isdefined(LS, :rules_lhs) - @test isdefined(LS, :rules_rhs) - @test isdefined(LS, :current_rules_lhs) - @test isdefined(LS, :current_rules_rhs) - @test isdefined(LS, :normal_forms) - @test isdefined(LS, :current_normal_forms) - end - - # ----------------------------------------------------------------------- - # FroidurePinTransf1 — representative element-typed type - # ----------------------------------------------------------------------- - @testset "FroidurePinTransf1 type exists" begin - @test isdefined(LS, :FroidurePinTransf1) - FPT1 = LS.FroidurePinTransf1 - @test FPT1 isa DataType || FPT1 isa UnionAll - end - - @testset "FroidurePinTransf1 element-typed methods" begin - FPT1 = LS.FroidurePinTransf1 - T1 = LS.Transf1 - - # Element access - @test hasmethod(LS.at, Tuple{FPT1, UInt}) - @test hasmethod(LS.sorted_at, Tuple{FPT1, UInt}) - @test hasmethod(LS.sorted_at_no_checks, Tuple{FPT1, UInt}) - @test hasmethod(LS.generator, Tuple{FPT1, UInt}) - @test hasmethod(LS.generator_no_checks, Tuple{FPT1, UInt}) - - # Containment / position - @test hasmethod(LS.contains, Tuple{FPT1, T1}) - @test hasmethod(LS.position, Tuple{FPT1, T1}) - @test hasmethod(LS.current_position, Tuple{FPT1, T1}) - @test hasmethod(LS.sorted_position, Tuple{FPT1, T1}) - @test hasmethod(LS.to_sorted_position, Tuple{FPT1, UInt}) - - # Fast product - @test hasmethod(LS.fast_product, Tuple{FPT1, UInt, UInt}) - @test hasmethod(LS.fast_product_no_checks, Tuple{FPT1, UInt, UInt}) - - # Idempotents - @test hasmethod(LS.number_of_idempotents, Tuple{FPT1}) - @test hasmethod(LS.is_idempotent, Tuple{FPT1, UInt}) - @test hasmethod(LS.is_idempotent_no_checks, Tuple{FPT1, UInt}) - - # Modification - @test hasmethod(LS.add_generator!, Tuple{FPT1, T1}) - @test hasmethod(LS.add_generator_no_checks!, Tuple{FPT1, T1}) - @test hasmethod(LS.closure!, Tuple{FPT1, T1}) - @test hasmethod(LS.reserve!, Tuple{FPT1, UInt}) - - # Display - @test isdefined(LS, :to_human_readable_repr) - - # Materialized collections - @test isdefined(LS, :idempotents) - @test isdefined(LS, :sorted_elements) - - # Word-element conversion - @test isdefined(LS, :to_element) - @test isdefined(LS, :to_element_no_checks) - @test isdefined(LS, :equal_to) - @test isdefined(LS, :equal_to_no_checks) - end - - # ----------------------------------------------------------------------- - # All 10 FroidurePin concrete types exist - # ----------------------------------------------------------------------- - @testset "All FroidurePin concrete types defined" begin - for sym in (:FroidurePinTransf1, :FroidurePinTransf2, :FroidurePinTransf4, - :FroidurePinPPerm1, :FroidurePinPPerm2, :FroidurePinPPerm4, - :FroidurePinPerm1, :FroidurePinPerm2, :FroidurePinPerm4, - :FroidurePinBMat8) - @test isdefined(LS, sym) - end - end - - # ----------------------------------------------------------------------- - # Smoke-test: construct and run a real FroidurePin via LibSemigroups.* - # directly (no high-level wrapper). This exercises the constructor - # lambdas and the size method across the C++ boundary. - # ----------------------------------------------------------------------- - @testset "FroidurePinTransf1 smoke test (LibSemigroups.* direct)" begin - # S₃ — symmetric group on 3 letters. - # Use Transf (high-level wrapper) to build the generators, then extract - # the raw C++ object (.cxx_obj) to pass to the CxxWrap constructor lambda. - # Generators in 1-based Julia: (1 2) → [2,1,3], (1 2 3) → [2,3,1]. - g1 = Transf([2, 1, 3]).cxx_obj # ::Transf1 (the CxxWrap type) - g2 = Transf([2, 3, 1]).cxx_obj - - fp = LS.FroidurePinTransf1(g1, g2) - @test LS.size(fp) == 6 - @test LS.number_of_generators(fp) == 2 - @test LS.degree(fp) == 3 - end - -end # @testset "FroidurePin binding surface" - - -# ============================================================================ -# Section 2: High-level integration tests (RED — wrapper doesn't exist yet) -# ============================================================================ - -# --------------------------------------------------------------------------- -# Parametric helper: basic size/collect contract -# --------------------------------------------------------------------------- -function check_fp_basic(gens, expected_size) - S = FroidurePin(gens...) - @test length(S) == expected_size - @test number_of_generators(S) == length(gens) - elts = collect(S) - @test length(elts) == expected_size - @test length(unique(elts)) == expected_size -end - -@testset "FroidurePin{E} high-level API" begin - - # ----------------------------------------------------------------------- - # Construction: type dispatch - # ----------------------------------------------------------------------- - @testset "Construction and type" begin - # S₃ from Transf generators - g1 = Transf([2, 1, 3]) # (1 2) in 1-based - g2 = Transf([2, 3, 1]) # (1 2 3) in 1-based - S = FroidurePin(g1, g2) - @test S isa FroidurePin{Transf{UInt8}} - - # PPerm generators - p1 = PPerm([2, 1, 3]) - p2 = PPerm([1, 3, 2]) - Sp = FroidurePin(p1, p2) - @test Sp isa FroidurePin{PPerm{UInt8}} - - # Perm generators - q1 = Perm([2, 1, 3]) - q2 = Perm([2, 3, 1]) - Sq = FroidurePin(q1, q2) - @test Sq isa FroidurePin{Perm{UInt8}} - - # BMat8 generators (2×2 blocks) - b1 = BMat8([[0, 1], [1, 0]]) - b2 = BMat8([[1, 0], [1, 1]]) - Sb = FroidurePin(b1, b2) - @test Sb isa FroidurePin{BMat8} - end - - # ----------------------------------------------------------------------- - # S₃ — primary test case throughout - # ----------------------------------------------------------------------- - @testset "S₃ — length and size" begin - S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) - @test length(S) == 6 - end - - @testset "S₃ — check_fp_basic parametric helper" begin - check_fp_basic([Transf([2, 1, 3]), Transf([2, 3, 1])], 6) - end - - @testset "S₃ — iteration / collect" begin - S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) - elts = collect(S) - @test elts isa Vector{Transf{UInt8}} - @test length(elts) == 6 - @test length(unique(elts)) == 6 - end - - @testset "S₃ — getindex (1-based)" begin - S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) - # Valid access - @test S[1] isa Transf{UInt8} - @test S[6] isa Transf{UInt8} - # Out-of-bounds (0-based is invalid) - @test_throws BoundsError S[0] - @test_throws BoundsError S[7] - end - - @testset "S₃ — in / contains" begin - S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) - @test Transf([2, 1, 3]) in S - @test Transf([1, 2, 3]) in S # identity is in S₃ - @test !(Transf([1, 1, 1]) in S) - end - - @testset "S₃ — push! adds a generator" begin - S = FroidurePin(Transf([2, 1, 3])) - n_before = number_of_generators(S) - push!(S, Transf([2, 3, 1])) - @test number_of_generators(S) == n_before + 1 - # After adding the second generator the semigroup is S₃ - @test length(S) == 6 - end - - @testset "S₃ — rules" begin - S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) - rs = rules(S) - @test rs isa Vector{Pair{Vector{Int}, Vector{Int}}} - # There must be at least one rule (|S₃| < |free monoid|) - @test length(rs) > 0 - # Each lhs and rhs must be non-empty 1-based generator-index vectors - for (lhs, rhs) in rs - @test all(x -> 1 <= x <= 2, lhs) - @test all(x -> 1 <= x <= 2, rhs) - end - end - - @testset "S₃ — minimal_factorisation (1-based)" begin - S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) - # Position 1 is always a generator — its factorisation is a length-1 word - w = minimal_factorisation(S, 1) - @test w isa Vector{Int} - @test length(w) == 1 - @test 1 <= w[1] <= 2 - # Out-of-bounds position - @test_throws Exception minimal_factorisation(S, 0) - @test_throws Exception minimal_factorisation(S, 7) - end - - @testset "S₃ — Cayley graphs" begin - S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) - rcg = right_cayley_graph(S) - lcg = left_cayley_graph(S) - # Should return something non-nothing (WordGraph or similar) - @test rcg !== nothing - @test lcg !== nothing - end - - @testset "S₃ — Runner interface" begin - S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) - @test !finished(S) # not yet run - run!(S) - @test finished(S) - end - - @testset "S₃ — idempotents" begin - S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) - ids = idempotents(S) - @test ids isa Vector{Transf{UInt8}} - # Every element in `ids` must satisfy x*x == x - for x in ids - @test x * x == x - end - # S₃ has exactly 1 idempotent (the identity) - @test length(ids) == 1 - end - - @testset "S₃ — sorted_elements" begin - S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) - se = sorted_elements(S) - @test se isa Vector{Transf{UInt8}} - @test length(se) == 6 - # Sorted means weakly increasing - for i in 2:length(se) - @test !(se[i] < se[i-1]) - end - end - - @testset "S₃ — number_of_generators" begin - S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) - @test number_of_generators(S) == 2 - end - - # ----------------------------------------------------------------------- - # Multiple element types: PPerm - # ----------------------------------------------------------------------- - @testset "PPerm generators" begin - # Symmetric inverse monoid on 2 points: pperm([2,1]) and pperm([], [], 2) - p1 = PPerm([2, 1]) - p2 = PPerm(Int[], Int[], 2) - S = FroidurePin(p1, p2) - @test S isa FroidurePin{PPerm{UInt8}} - elts = collect(S) - @test length(elts) > 0 - for x in elts - @test x isa PPerm{UInt8} - end - end - - # ----------------------------------------------------------------------- - # Multiple element types: Perm - # ----------------------------------------------------------------------- - @testset "Perm generators — S₃" begin - q1 = Perm([2, 1, 3]) - q2 = Perm([2, 3, 1]) - S = FroidurePin(q1, q2) - @test S isa FroidurePin{Perm{UInt8}} - @test length(S) == 6 - elts = collect(S) - @test all(x -> x isa Perm{UInt8}, elts) - end - - # ----------------------------------------------------------------------- - # Multiple element types: BMat8 - # ----------------------------------------------------------------------- - @testset "BMat8 generators" begin - # Small semigroup: 2 boolean 2×2 matrices - b1 = BMat8([[0, 1], [1, 0]]) - b2 = BMat8([[1, 0], [1, 1]]) - S = FroidurePin(b1, b2) - @test S isa FroidurePin{BMat8} - @test length(S) > 0 - elts = collect(S) - @test all(x -> x isa BMat8, elts) - end - - # ----------------------------------------------------------------------- - # Larger example: 5-generator transformation semigroup of degree 6 - # Reference: U from test-froidure-pin-transf.cpp test 049 → size 7776 - # ----------------------------------------------------------------------- - @testset "5-generator Transf degree-6 semigroup" begin - gens = [ - 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]), - ] - check_fp_basic(gens, 7776) - end - - # ----------------------------------------------------------------------- - # current_size: before full enumeration - # ----------------------------------------------------------------------- - @testset "current_size before enumerate" begin - S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) - # Without enumerating, current_size == number of generators - @test current_size(S) == 2 - # After enumerate!, current_size == full size - enumerate!(S, 100) - @test current_size(S) == 6 - end - - # ----------------------------------------------------------------------- - # degree - # ----------------------------------------------------------------------- - @testset "degree" begin - S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) - @test degree(S) == 3 - end - - # ----------------------------------------------------------------------- - # fast_product - # ----------------------------------------------------------------------- - @testset "fast_product (0-based internally, 1-based Julia)" begin - S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) - # Ensure the semigroup is fully enumerated - run!(S) - # product of element 1 and element 2 (1-based) must be in [1, 6] - idx = fast_product(S, 1, 2) - @test 1 <= idx <= 6 - end - - # ----------------------------------------------------------------------- - # is_idempotent (1-based position) - # ----------------------------------------------------------------------- - @testset "is_idempotent (1-based)" begin - S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) - run!(S) - for i in 1:length(S) - x = S[i] - @test is_idempotent(S, i) == (x * x == x) - end - end - - # ----------------------------------------------------------------------- - # to_element: word → element round-trip - # ----------------------------------------------------------------------- - @testset "to_element word round-trip" begin - S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) - run!(S) - # Generator 1 (1-based) is S[1] or S[position_of_generator(S,1)] - w = [1] # word consisting of first generator - x = to_element(S, w) - @test x isa Transf{UInt8} - # Factorising x should give back a word whose product = x - wf = minimal_factorisation(S, Semigroups.position(S, x)) - @test to_element(S, wf) == x - end - -end # @testset "FroidurePin{E} high-level API" From 75278746ade6c029c351fb39dac2354ea894916a Mon Sep 17 00:00:00 2001 From: James Swent Date: Fri, 24 Apr 2026 10:50:50 +0100 Subject: [PATCH 14/17] chore: run formatter again --- deps/src/froidure-pin-base.cpp | 189 +++++++++++++++------------------ deps/src/froidure-pin.cpp | 149 +++++++++++++------------- src/Semigroups.jl | 2 +- src/froidure-pin.jl | 47 +++++--- test/test_bmat8.jl | 6 +- 5 files changed, 192 insertions(+), 201 deletions(-) diff --git a/deps/src/froidure-pin-base.cpp b/deps/src/froidure-pin-base.cpp index 016f33c..50a0eb5 100644 --- a/deps/src/froidure-pin-base.cpp +++ b/deps/src/froidure-pin-base.cpp @@ -53,39 +53,35 @@ namespace libsemigroups_julia { // 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()); + 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(); - }); + 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); - }); + 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(); - }); + 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(); - }); + 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 { @@ -99,16 +95,14 @@ namespace libsemigroups_julia { }); // enumerate! - Enumerate until at least `limit` elements are found - type.method("enumerate!", - [](FroidurePinBase& self, size_t limit) { - self.enumerate(limit); - }); + 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(); - }); + 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", @@ -123,10 +117,9 @@ namespace libsemigroups_julia { }); // contains_one - Is the identity an element? (triggers full enumeration) - type.method("contains_one", - [](FroidurePinBase& self) -> bool { - return self.contains_one(); - }); + 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", @@ -142,12 +135,11 @@ namespace libsemigroups_julia { // 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); - }); + 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 @@ -204,10 +196,9 @@ namespace libsemigroups_julia { }); // length / length_no_checks (trigger full enumeration) - type.method("length", - [](FroidurePinBase& self, uint32_t pos) -> size_t { - return self.length(pos); - }); + 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); @@ -228,17 +219,16 @@ namespace libsemigroups_julia { //////////////////////////////////////////////////////////////////////// // 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); - }); + 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 { + [](FroidurePinBase const& fpb, uint32_t pos) -> word_type { return libsemigroups::froidure_pin:: current_minimal_factorisation_no_checks(fpb, pos); }); @@ -246,8 +236,8 @@ namespace libsemigroups_julia { // 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); + return libsemigroups::froidure_pin::minimal_factorisation(fpb, + pos); }); // factorisation (checked, triggers partial enumeration) @@ -262,7 +252,7 @@ namespace libsemigroups_julia { // froidure_pin::current_position (checked, no enumeration) m.method("current_position", - [](FroidurePinBase const& fpb, + [](FroidurePinBase const& fpb, jlcxx::ArrayRef arr) -> uint32_t { word_type w(arr.begin(), arr.end()); return libsemigroups::froidure_pin::current_position(fpb, w); @@ -270,7 +260,7 @@ namespace libsemigroups_julia { // froidure_pin::current_position_no_checks (unchecked, no enumeration) m.method("current_position_no_checks", - [](FroidurePinBase const& fpb, + [](FroidurePinBase const& fpb, jlcxx::ArrayRef arr) -> uint32_t { word_type w(arr.begin(), arr.end()); return libsemigroups::froidure_pin::current_position_no_checks( @@ -279,37 +269,32 @@ namespace libsemigroups_julia { // froidure_pin::position (checked, triggers full enumeration) m.method("position", - [](FroidurePinBase& fpb, - jlcxx::ArrayRef arr) -> uint32_t { + [](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 { + [](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); - }); + 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); - }); + 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 @@ -318,34 +303,30 @@ namespace libsemigroups_julia { // right_cayley_graph (triggers full enumeration) type.method( "right_cayley_graph", - [](FroidurePinBase& self) - -> FroidurePinBase::cayley_graph_type const& { + [](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(); - }); + 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& { + [](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(); - }); + 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 @@ -356,27 +337,25 @@ namespace libsemigroups_julia { // 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_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; - }); + 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", diff --git a/deps/src/froidure-pin.cpp b/deps/src/froidure-pin.cpp index 357b0cd..f7ba4c9 100644 --- a/deps/src/froidure-pin.cpp +++ b/deps/src/froidure-pin.cpp @@ -41,33 +41,42 @@ namespace jlcxx { // IsMirroredType — one per concrete FroidurePin instantiation template <> - struct IsMirroredType>> + struct IsMirroredType< + libsemigroups::FroidurePin>> : std::false_type {}; template <> - struct IsMirroredType>> + struct IsMirroredType< + libsemigroups::FroidurePin>> : std::false_type {}; template <> - struct IsMirroredType>> + struct IsMirroredType< + libsemigroups::FroidurePin>> : std::false_type {}; template <> - struct IsMirroredType>> + struct IsMirroredType< + libsemigroups::FroidurePin>> : std::false_type {}; template <> - struct IsMirroredType>> + struct IsMirroredType< + libsemigroups::FroidurePin>> : std::false_type {}; template <> - struct IsMirroredType>> + struct IsMirroredType< + libsemigroups::FroidurePin>> : std::false_type {}; template <> - struct IsMirroredType>> + struct IsMirroredType< + libsemigroups::FroidurePin>> : std::false_type {}; template <> - struct IsMirroredType>> + struct IsMirroredType< + libsemigroups::FroidurePin>> : std::false_type {}; template <> - struct IsMirroredType>> + struct IsMirroredType< + libsemigroups::FroidurePin>> : std::false_type {}; template <> @@ -120,11 +129,10 @@ namespace libsemigroups_julia { 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()); - }); + 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) @@ -134,26 +142,23 @@ namespace libsemigroups_julia { 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); }); + 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); - }); + 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); - }); + 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); - }); + 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", @@ -164,16 +169,14 @@ namespace libsemigroups_julia { //////////////////////////////////////////////////////////////////// // contains(FP&, E const&) -> bool - type.method("contains", - [](FP& self, E const& x) -> bool { - return self.contains(x); - }); + 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); - }); + 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", @@ -182,16 +185,14 @@ namespace libsemigroups_julia { }); // sorted_position(FP&, E const&) -> element_index_type - type.method("sorted_position", - [](FP& self, E const& x) -> uint32_t { - return self.sorted_position(x); - }); + 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); - }); + type.method("to_sorted_position", [](FP& self, size_t i) -> uint32_t { + return self.to_sorted_position(i); + }); //////////////////////////////////////////////////////////////////// // 4. Fast product @@ -211,20 +212,17 @@ namespace libsemigroups_julia { // 5. Idempotents //////////////////////////////////////////////////////////////////// - type.method("number_of_idempotents", - [](FP& self) -> size_t { - return self.number_of_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", [](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); - }); + type.method("is_idempotent_no_checks", [](FP& self, size_t i) -> bool { + return self.is_idempotent_no_checks(i); + }); //////////////////////////////////////////////////////////////////// // 6. Modification @@ -235,10 +233,9 @@ namespace libsemigroups_julia { [](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); - }); + 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) { @@ -248,18 +245,16 @@ namespace libsemigroups_julia { // 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()); - }); + 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()); - }); + 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}) @@ -281,7 +276,7 @@ namespace libsemigroups_julia { // equal_to — two words m.method("equal_to", - [](FP const& self, + [](FP const& self, jlcxx::ArrayRef arr1, jlcxx::ArrayRef arr2) -> bool { word_type w1(arr1.begin(), arr1.end()); @@ -292,7 +287,7 @@ namespace libsemigroups_julia { // equal_to_no_checks m.method("equal_to_no_checks", - [](FP const& self, + [](FP const& self, jlcxx::ArrayRef arr1, jlcxx::ArrayRef arr2) -> bool { word_type w1(arr1.begin(), arr1.end()); @@ -333,17 +328,15 @@ namespace libsemigroups_julia { // 9. Memory //////////////////////////////////////////////////////////////////// - type.method( - "reserve!", [](FP& self, size_t val) { self.reserve(val); }); + 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); - }); + m.method("to_human_readable_repr", [](FP const& self) -> std::string { + return libsemigroups::to_human_readable_repr(self); + }); } } // anonymous namespace diff --git a/src/Semigroups.jl b/src/Semigroups.jl index b6251be..f8f88e1 100644 --- a/src/Semigroups.jl +++ b/src/Semigroups.jl @@ -167,7 +167,7 @@ export left_one, right_one # FroidurePin export FroidurePin, current_size, number_of_generators, enumerate! export generator, sorted_at -export position, sorted_position, to_sorted_position +export sorted_position, to_sorted_position export closure!, copy_closure, copy_add_generators, reserve! export batch_size, set_batch_size! export current_position diff --git a/src/froidure-pin.jl b/src/froidure-pin.jl index 0096d01..9e0960f 100644 --- a/src/froidure-pin.jl +++ b/src/froidure-pin.jl @@ -197,7 +197,7 @@ function FroidurePin(gens::Vector{E}) where {E} else # >4 generators: construct with first, then add the rest cxx_obj = @wrap_libsemigroups_call FPType(cxx_gens[1]) - for i in 2:n + for i = 2:n @wrap_libsemigroups_call LibSemigroups.add_generator!(cxx_obj, cxx_gens[i]) end end @@ -298,7 +298,8 @@ This partially enumerates the semigroup. After calling this, `current_size` will be at least `min(limit, length(fp))`. """ function enumerate!(fp::FroidurePin, limit::Integer) - @wrap_libsemigroups_call LibSemigroups.enumerate!(fp.cxx_obj, UInt(limit)) + lim = UInt(limit) + @wrap_libsemigroups_call LibSemigroups.enumerate!(fp.cxx_obj, lim) return fp end @@ -452,7 +453,8 @@ running_for(fp::FroidurePin) = LibSemigroups.running_for(fp.cxx_obj) Return the duration of the most recent `run_for!` call as a `Dates.Nanosecond`. """ -running_for_how_long(fp::FroidurePin) = Nanosecond(LibSemigroups.running_for_how_long(fp.cxx_obj)) +running_for_how_long(fp::FroidurePin) = + Nanosecond(LibSemigroups.running_for_how_long(fp.cxx_obj)) """ running_until(fp::FroidurePin) -> Bool @@ -590,7 +592,7 @@ Create an independent copy of the semigroup by reconstructing it from its generators. """ function Base.copy(fp::FroidurePin{E}) where {E} - gens = [generator(fp, i) for i in 1:number_of_generators(fp)] + gens = [generator(fp, i) for i = 1:number_of_generators(fp)] return FroidurePin(gens) end @@ -823,7 +825,8 @@ hint and does not affect correctness. Returns `fp` for method chaining. """ function reserve!(fp::FroidurePin, n::Integer) - @wrap_libsemigroups_call LibSemigroups.reserve!(fp.cxx_obj, UInt(n)) + val = UInt(n) + @wrap_libsemigroups_call LibSemigroups.reserve!(fp.cxx_obj, val) return fp end @@ -960,7 +963,8 @@ number_of_rules(fp::FroidurePin) = Int(LibSemigroups.number_of_rules(fp.cxx_obj) 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)) +current_number_of_rules(fp::FroidurePin) = + Int(LibSemigroups.current_number_of_rules(fp.cxx_obj)) """ number_of_idempotents(fp::FroidurePin) -> Int @@ -969,7 +973,8 @@ 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)) +number_of_idempotents(fp::FroidurePin) = + Int(LibSemigroups.number_of_idempotents(fp.cxx_obj)) """ currently_contains_one(fp::FroidurePin) -> Bool @@ -985,7 +990,8 @@ currently_contains_one(fp::FroidurePin) = LibSemigroups.currently_contains_one(f 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)) +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 @@ -1004,7 +1010,9 @@ 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))) + return Int( + LibSemigroups.number_of_elements_of_length_range(fp.cxx_obj, UInt(min), UInt(max)), + ) end """ @@ -1088,7 +1096,7 @@ function rules(fp::FroidurePin) 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 in 1:n + for i = 1:n result[i] = _word_from_cpp(lhs_raw[i]) => _word_from_cpp(rhs_raw[i]) end return result @@ -1105,7 +1113,7 @@ function current_rules(fp::FroidurePin) 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 in 1:n + for i = 1:n result[i] = _word_from_cpp(lhs_raw[i]) => _word_from_cpp(rhs_raw[i]) end return result @@ -1210,7 +1218,10 @@ 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) + raw = @wrap_libsemigroups_call LibSemigroups.current_minimal_factorisation( + fp.cxx_obj, + idx, + ) return _word_from_cpp(raw) end @@ -1270,7 +1281,8 @@ right_cayley_graph(fp::FroidurePin) = LibSemigroups.right_cayley_graph(fp.cxx_ob Return the right Cayley graph for elements enumerated so far. """ -current_right_cayley_graph(fp::FroidurePin) = LibSemigroups.current_right_cayley_graph(fp.cxx_obj) +current_right_cayley_graph(fp::FroidurePin) = + LibSemigroups.current_right_cayley_graph(fp.cxx_obj) """ left_cayley_graph(fp::FroidurePin) -> WordGraph @@ -1286,7 +1298,8 @@ left_cayley_graph(fp::FroidurePin) = LibSemigroups.left_cayley_graph(fp.cxx_obj) Return the left Cayley graph for elements enumerated so far. """ -current_left_cayley_graph(fp::FroidurePin) = LibSemigroups.current_left_cayley_graph(fp.cxx_obj) +current_left_cayley_graph(fp::FroidurePin) = + LibSemigroups.current_left_cayley_graph(fp.cxx_obj) # ============================================================================ # Word-element conversion @@ -1320,7 +1333,11 @@ 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}) +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) 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) From 2fe487f3e9cdcaabbca23ac6e30778556f4c970a Mon Sep 17 00:00:00 2001 From: James Swent Date: Fri, 24 Apr 2026 11:38:46 +0100 Subject: [PATCH 15/17] docs: Update froidure-pin.jl documentation --- src/froidure-pin.jl | 761 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 669 insertions(+), 92 deletions(-) diff --git a/src/froidure-pin.jl b/src/froidure-pin.jl index 9e0960f..983dc79 100644 --- a/src/froidure-pin.jl +++ b/src/froidure-pin.jl @@ -130,25 +130,34 @@ _fp_element_type(::Type{T}) where {T<:BMat8} = BMat8 """ FroidurePin{E} -A Froidure-Pin semigroup over elements of type `E`. +Type implementing the Froidure-Pin algorithm. -The Froidure-Pin algorithm is used to enumerate all elements of a finitely -generated semigroup. This type wraps the C++ `FroidurePin` class from -libsemigroups. +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}`, `Transf{UInt16}`, `Transf{UInt32}` (transformations) -- `PPerm{UInt8}`, `PPerm{UInt16}`, `PPerm{UInt32}` (partial permutations) -- `Perm{UInt8}`, `Perm{UInt16}`, `Perm{UInt32}` (permutations) -- `BMat8` (boolean matrices up to 8x8) +- [`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 (S_3) +length(S) # 6 (symmetric group S_3) ``` + +# See also +- [`FroidurePinBase`](@ref Semigroups.FroidurePinBase) +- [`Runner`](@ref Semigroups.Runner) """ mutable struct FroidurePin{E} cxx_obj::_FroidurePinCxx @@ -161,7 +170,21 @@ end """ FroidurePin(gens::Vector{E}) where {E} -Construct a `FroidurePin{E}` from a vector of generators. +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 @@ -170,6 +193,9 @@ 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") @@ -208,7 +234,19 @@ end """ FroidurePin(x::E, xs::E...) where {E} -Construct a `FroidurePin{E}` from one or more generators (variadic). +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 @@ -217,6 +255,9 @@ 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...]) @@ -229,9 +270,17 @@ end """ Base.length(fp::FroidurePin) -> Int -Return the total number of elements in the semigroup. +Return the size of a [`FroidurePin`](@ref Semigroups.FroidurePin) instance. -Triggers full enumeration if not already complete. +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 @@ -240,14 +289,24 @@ 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 enumerated so far (without triggering -further enumeration). +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 @@ -256,13 +315,25 @@ 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 the elements in the semigroup. +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 @@ -277,7 +348,13 @@ degree(fp::FroidurePin) = Int(LibSemigroups.degree(fp.cxx_obj)) """ number_of_generators(fp::FroidurePin) -> Int -Return the number of generators of the semigroup. +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 @@ -286,16 +363,43 @@ 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) + 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)`. -Enumerate elements until at least `limit` elements have been found. +# Example +```julia +using Semigroups + +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +enumerate!(S, 4) +current_size(S) # >= 4 +``` -This partially enumerates the semigroup. After calling this, `current_size` -will be at least `min(limit, length(fp))`. +# See also +- [`run!`](@ref) +- [`current_size`](@ref) """ function enumerate!(fp::FroidurePin, limit::Integer) lim = UInt(limit) @@ -314,7 +418,13 @@ end Run the Froidure-Pin algorithm until [`finished`](@ref). -Returns `fp` for method chaining. +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) @@ -326,13 +436,25 @@ end Run the Froidure-Pin algorithm for a specified amount of time. -Returns `fp` for method chaining. +At the end of this call `fp` is either [`finished`](@ref), +[`dead`](@ref), or [`timed_out`](@ref). Returns `fp` for method +chaining. -# Examples +# 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) @@ -354,24 +476,44 @@ end Run the Froidure-Pin algorithm until a nullary predicate returns `true` or [`finished`](@ref). -Returns `fp` for method chaining. +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 - some_condition(S) + 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 object, resetting it to its default state. +Initialize an existing [`FroidurePin`](@ref Semigroups.FroidurePin) +object. -Returns `fp` for method chaining. +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) @@ -381,77 +523,170 @@ end """ kill!(fp::FroidurePin) -Stop the Froidure-Pin algorithm from running (thread-safe). +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 the Froidure-Pin algorithm has been run to completion. +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 the Froidure-Pin algorithm has been run to completion successfully. +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 the Froidure-Pin algorithm has been started. +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 the Froidure-Pin algorithm is currently running. +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 last `run_for!` call timed out. +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 Froidure-Pin algorithm is stopped for any reason. +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 Froidure-Pin algorithm has been killed by another thread. +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 algorithm was stopped by the predicate passed to `run_until!`. +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 the algorithm is currently running for a particular length of time. +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 duration of the most recent `run_for!` call as a `Dates.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)) @@ -459,28 +694,60 @@ running_for_how_long(fp::FroidurePin) = """ running_until(fp::FroidurePin) -> Bool -Check if the algorithm is currently running until a predicate returns `true`. +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 Froidure-Pin algorithm. +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 the Froidure-Pin algorithm stopped. +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 the algorithm stopped. +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) @@ -491,9 +758,21 @@ string_why_we_stopped(fp::FroidurePin) = LibSemigroups.string_why_we_stopped(fp. """ Base.getindex(fp::FroidurePin{E}, i::Integer) -> E -Return the `i`-th element of the semigroup (1-based indexing). +Access the element with index `i`. -Triggers full enumeration if not already complete. +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 @@ -501,8 +780,9 @@ S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) S[1] # first element ``` -# Throws -- `BoundsError` if `i` is out of range. +# See also +- [`sorted_at`](@ref) +- [`generator`](@ref) """ function Base.getindex(fp::FroidurePin{E}, i::Integer) where {E} if i < 1 || i > length(fp) @@ -516,13 +796,36 @@ end """ generator(fp::FroidurePin{E}, i::Integer) -> E -Return the `i`-th generator of the semigroup (1-based indexing). +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) @@ -533,13 +836,31 @@ end """ sorted_at(fp::FroidurePin{E}, i::Integer) -> E -Return the `i`-th element in sorted order (1-based indexing). +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) @@ -552,10 +873,18 @@ end # ============================================================================ """ - Base.iterate(fp::FroidurePin, state=1) + 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])) @@ -564,6 +893,10 @@ for x in S 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) @@ -573,9 +906,12 @@ function Base.iterate(fp::FroidurePin, state::Int = 1) end """ - Base.eltype(::Type{FroidurePin{E}}) where E + Base.eltype(::Type{FroidurePin{E}}) where E -> Type + +Return the element type `E` of a [`FroidurePin{E}`](@ref Semigroups.FroidurePin). -Return the element type `E` of a `FroidurePin{E}`. +# Complexity +Constant. """ Base.eltype(::Type{FroidurePin{E}}) where {E} = E @@ -588,8 +924,25 @@ Base.IteratorSize(::Type{<:FroidurePin}) = Base.HasLength() """ Base.copy(fp::FroidurePin{E}) -> FroidurePin{E} -Create an independent copy of the semigroup by reconstructing it from -its generators. +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)] @@ -603,7 +956,11 @@ end """ Base.show(io::IO, fp::FroidurePin) -Display a human-readable representation of the semigroup. +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)) @@ -616,9 +973,17 @@ end """ Base.in(x::E, fp::FroidurePin{E}) where E -> Bool -Check whether the element `x` belongs to the semigroup `fp`. +Test membership of an element. -Triggers full enumeration if not already complete. +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 @@ -626,6 +991,10 @@ 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) @@ -639,17 +1008,30 @@ function Base.in(x::BMat8, fp::FroidurePin{BMat8}) end """ - position(fp::FroidurePin{E}, x::E) where E -> Int + position(fp::FroidurePin{E}, x::E) where E -> Union{Int, UNDEFINED} -Return the 1-based position of element `x` in the semigroup `fp`. +Find the position of an element with enumeration if necessary. -Triggers full enumeration if not already complete. +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) @@ -664,11 +1046,26 @@ function position(fp::FroidurePin{BMat8}, x::BMat8) end """ - sorted_position(fp::FroidurePin{E}, x::E) where E -> Int + sorted_position(fp::FroidurePin{E}, x::E) where E -> Union{Int, UNDEFINED} -Return the 1-based sorted position of element `x` in the semigroup `fp`. +Return the sorted index of an element. -Triggers full enumeration if not already complete. +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) @@ -683,11 +1080,24 @@ function sorted_position(fp::FroidurePin{BMat8}, x::BMat8) end """ - to_sorted_position(fp::FroidurePin, i::Integer) -> Int + to_sorted_position(fp::FroidurePin, i::Integer) -> Union{Int, UNDEFINED} -Convert a 1-based element position to its 1-based sorted position. +Return the sorted index of an element via its index. -Triggers full enumeration if not already complete. +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) @@ -696,12 +1106,26 @@ function to_sorted_position(fp::FroidurePin, i::Integer) end """ - current_position(fp::FroidurePin{E}, x::E) where E -> Int + 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`. -Return the 1-based position of element `x` among the elements -enumerated so far (without triggering further enumeration). +# Arguments +- `fp::FroidurePin{E}`: the FroidurePin instance. +- `x::E`: an element whose position is sought. -Returns `UNDEFINED` if `x` has not yet been enumerated. +!!! 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) @@ -716,13 +1140,33 @@ function current_position(fp::FroidurePin{BMat8}, x::BMat8) end """ - current_position(fp::FroidurePin, w::AbstractVector{<:Integer}) -> Int + current_position(fp::FroidurePin, w::AbstractVector{<:Integer}) -> Union{Int, UNDEFINED} -Return the 1-based position of the element represented by the 1-based -generator-index word `w` among the elements enumerated so far -(without triggering further enumeration). +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`. -Returns `UNDEFINED` if the element has not yet been enumerated. +!!! 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) @@ -837,16 +1281,46 @@ end """ batch_size(fp::FroidurePin) -> Int -Return the current batch size used for partial enumeration. +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 the batch size for partial enumeration. +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)) @@ -860,19 +1334,42 @@ end """ contains_one(fp::FroidurePin) -> Bool -Check whether the semigroup contains the identity element. +Check if the multiplicative identity is an element of `fp`. -Triggers full enumeration if not already complete. +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 whether the element at 1-based position `i` is an idempotent -(i.e., `x * x == x`). +Check if the element at 1-based position `i` is an idempotent. -Triggers full enumeration if not already complete. +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) @@ -886,9 +1383,29 @@ end """ prefix(fp::FroidurePin, i::Integer) -> Int -Return the 1-based position of the prefix of the element at 1-based -position `i`. The prefix is the element obtained by removing the last -letter of the factorisation. +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) @@ -899,9 +1416,29 @@ end """ suffix(fp::FroidurePin, i::Integer) -> Int -Return the 1-based position of the suffix of the element at 1-based -position `i`. The suffix is the element obtained by removing the first -letter of the factorisation. +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) @@ -912,8 +1449,28 @@ end """ first_letter(fp::FroidurePin, i::Integer) -> Int -Return the 1-based position of the first letter (generator) in the -factorisation of the element at 1-based position `i`. +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) @@ -924,8 +1481,28 @@ end """ final_letter(fp::FroidurePin, i::Integer) -> Int -Return the 1-based position of the final letter (generator) in the -factorisation of the element at 1-based position `i`. +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) From 9139a8cb3d2c698698103ec0cc436787801b93fe Mon Sep 17 00:00:00 2001 From: James Swent Date: Fri, 24 Apr 2026 11:44:29 +0100 Subject: [PATCH 16/17] docs: add Froidure-Pin doc pages --- docs/make.jl | 5 + .../froidure-pin/froidure-pin.md | 204 ++++++++++++++++++ .../main-algorithms/froidure-pin/helpers.md | 100 +++++++++ .../src/main-algorithms/froidure-pin/index.md | 22 ++ docs/src/main-algorithms/index.md | 24 ++- 5 files changed, 344 insertions(+), 11 deletions(-) create mode 100644 docs/src/main-algorithms/froidure-pin/froidure-pin.md create mode 100644 docs/src/main-algorithms/froidure-pin/helpers.md create mode 100644 docs/src/main-algorithms/froidure-pin/index.md 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..c393fb0 --- /dev/null +++ b/docs/src/main-algorithms/froidure-pin/helpers.md @@ -0,0 +1,100 @@ +# 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 three +groups: factorisations, collections, and word-element conversion. + +## 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) From 5d8390dc507c37b756bf6a7aed1cacca87f26363 Mon Sep 17 00:00:00 2001 From: James Swent Date: Fri, 24 Apr 2026 11:45:31 +0100 Subject: [PATCH 17/17] docs: add table of contents for fp helpers --- docs/src/main-algorithms/froidure-pin/helpers.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/src/main-algorithms/froidure-pin/helpers.md b/docs/src/main-algorithms/froidure-pin/helpers.md index c393fb0..47354d9 100644 --- a/docs/src/main-algorithms/froidure-pin/helpers.md +++ b/docs/src/main-algorithms/froidure-pin/helpers.md @@ -2,8 +2,18 @@ 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 three -groups: factorisations, collections, and word-element conversion. +`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