diff --git a/Project.toml b/Project.toml index b0f2640..e35c851 100644 --- a/Project.toml +++ b/Project.toml @@ -1,19 +1,20 @@ name = "Semigroups" uuid = "f8a5e1c0-7b2d-4a3e-9c6f-1d2e3f4a5b6c" -authors = ["James Swent"] version = "0.1.0" +authors = ["James Swent"] [deps] -CxxWrap = "1f15a43c-97ca-5a2a-ae31-89f07a497df4" AbstractAlgebra = "c3fe647b-3220-5bb0-a1ea-a7954cac585d" +CxxWrap = "1f15a43c-97ca-5a2a-ae31-89f07a497df4" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[compat] +AbstractAlgebra = "0.43, 0.48" +CxxWrap = "0.17" +julia = "1.9" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] test = ["Test"] - -[compat] -julia = "1.9" -CxxWrap = "0.17" -AbstractAlgebra = "0.43, 0.48" diff --git a/deps/src/CMakeLists.txt b/deps/src/CMakeLists.txt index 888d929..7bd5eaf 100644 --- a/deps/src/CMakeLists.txt +++ b/deps/src/CMakeLists.txt @@ -33,7 +33,11 @@ message(STATUS "libsemigroups libraries: ${LIBSEMIGROUPS_LIBRARIES}") add_library(libsemigroups_julia SHARED libsemigroups_julia.cpp constants.cpp + runner.cpp + word-graph.cpp + froidure-pin-base.cpp transf.cpp + froidure-pin.cpp ) # Include directories diff --git a/deps/src/froidure-pin-base.cpp b/deps/src/froidure-pin-base.cpp new file mode 100644 index 0000000..eb450bf --- /dev/null +++ b/deps/src/froidure-pin-base.cpp @@ -0,0 +1,317 @@ +// +// Semigroups.jl +// Copyright (C) 2026, James W. Swent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "libsemigroups_julia.hpp" + +#include + +#include +#include +#include + +// Explicit SuperType specialization for CxxWrap upcasting. +// Enables CxxBaseRef to upcast to Runner when calling +// inherited Runner methods (finished, run!, etc.) on FroidurePinBase instances. +namespace jlcxx { +template <> struct SuperType { + typedef libsemigroups::Runner type; +}; +} // namespace jlcxx + +namespace libsemigroups_julia { + +void define_froidure_pin_base(jl::Module & m) +{ + using libsemigroups::FroidurePinBase; + using libsemigroups::Runner; + using libsemigroups::word_type; + using libsemigroups::froidure_pin::product_by_reduction; + + using FPB = FroidurePinBase; + using WG = FPB::cayley_graph_type; + + // Register FroidurePinBase inheriting from Runner + auto type = + m.add_type("FroidurePinBase", jlcxx::julia_base_type()); + + ////////////////////////////////////////////////////////////////////////// + // Settings + ////////////////////////////////////////////////////////////////////////// + + // batch_size - getter + type.method("batch_size", [](FPB const & self) -> size_t { + return self.batch_size(); + }); + + // set_batch_size! - setter (different name to avoid CxxWrap overload issue) + type.method("set_batch_size!", [](FPB & self, size_t val) -> FPB & { + return self.batch_size(val); + }); + + ////////////////////////////////////////////////////////////////////////// + // Size and enumeration + ////////////////////////////////////////////////////////////////////////// + + // current_size - number of elements enumerated so far (no enumeration) + type.method("current_size", [](FPB const & self) -> size_t { + return self.current_size(); + }); + + // size - full enumeration, returns total size + type.method("size", [](FPB & self) -> size_t { + return self.size(); + }); + + // degree - degree of elements + type.method("degree", [](FPB const & self) -> size_t { + return self.degree(); + }); + + // enumerate - enumerate up to limit elements + type.method("enumerate", [](FPB & self, size_t limit) { + self.enumerate(limit); + }); + + ////////////////////////////////////////////////////////////////////////// + // Rules + ////////////////////////////////////////////////////////////////////////// + + // number_of_rules - total (triggers full enumeration) + type.method("number_of_rules", [](FPB & self) -> size_t { + return self.number_of_rules(); + }); + + // current_number_of_rules - so far enumerated (no enumeration) + type.method("current_number_of_rules", [](FPB const & self) -> size_t { + return self.current_number_of_rules(); + }); + + ////////////////////////////////////////////////////////////////////////// + // Identity element + ////////////////////////////////////////////////////////////////////////// + + // contains_one - triggers full enumeration + type.method("contains_one", [](FPB & self) -> bool { + return self.contains_one(); + }); + + // currently_contains_one - no enumeration + type.method("currently_contains_one", [](FPB const & self) -> bool { + return self.currently_contains_one(); + }); + + ////////////////////////////////////////////////////////////////////////// + // Position queries + ////////////////////////////////////////////////////////////////////////// + + // position_of_generator - position of i-th generator + type.method("position_of_generator", [](FPB const & self, uint32_t i) -> uint32_t { + return self.position_of_generator(i); + }); + + ////////////////////////////////////////////////////////////////////////// + // Prefix / suffix / first / final letter + ////////////////////////////////////////////////////////////////////////// + + type.method("prefix", [](FPB const & self, uint32_t pos) -> uint32_t { + return self.prefix(pos); + }); + + type.method("suffix", [](FPB const & self, uint32_t pos) -> uint32_t { + return self.suffix(pos); + }); + + type.method("first_letter", [](FPB const & self, uint32_t pos) -> uint32_t { + return self.first_letter(pos); + }); + + type.method("final_letter", [](FPB const & self, uint32_t pos) -> uint32_t { + return self.final_letter(pos); + }); + + ////////////////////////////////////////////////////////////////////////// + // Word lengths + ////////////////////////////////////////////////////////////////////////// + + // current_length - no enumeration + type.method("current_length", [](FPB const & self, uint32_t pos) -> size_t { + return self.current_length(pos); + }); + + // length - triggers enumeration + type.method("length", [](FPB & self, uint32_t pos) -> size_t { + return self.length(pos); + }); + + // current_max_word_length - no enumeration + type.method("current_max_word_length", [](FPB const & self) -> size_t { + return self.current_max_word_length(); + }); + + ////////////////////////////////////////////////////////////////////////// + // Number of elements by length + ////////////////////////////////////////////////////////////////////////// + + // number_of_elements_of_length - single length (no enumeration) + type.method("number_of_elements_of_length", [](FPB const & self, size_t len) -> size_t { + return self.number_of_elements_of_length(len); + }); + + // number_of_elements_of_length_range - range [min, max) (no enumeration) + type.method("number_of_elements_of_length_range", + [](FPB const & self, size_t min, size_t max) -> size_t { + return self.number_of_elements_of_length(min, max); + }); + + ////////////////////////////////////////////////////////////////////////// + // Cayley graphs (return by copy for safety) + ////////////////////////////////////////////////////////////////////////// + + // right_cayley_graph - triggers full enumeration + type.method("right_cayley_graph", [](FPB & self) -> WG { + return self.right_cayley_graph(); + }); + + // left_cayley_graph - triggers full enumeration + type.method("left_cayley_graph", [](FPB & self) -> WG { + return self.left_cayley_graph(); + }); + + // current_right_cayley_graph - no enumeration + type.method("current_right_cayley_graph", [](FPB const & self) -> WG { + return self.current_right_cayley_graph(); + }); + + // current_left_cayley_graph - no enumeration + type.method("current_left_cayley_graph", [](FPB const & self) -> WG { + return self.current_left_cayley_graph(); + }); + + ////////////////////////////////////////////////////////////////////////// + // Free functions: product_by_reduction + ////////////////////////////////////////////////////////////////////////// + + m.method("product_by_reduction", + [](FPB const & fpb, uint32_t i, uint32_t j) -> uint32_t { + return product_by_reduction(fpb, i, j); + }); + + ////////////////////////////////////////////////////////////////////////// + // Free functions: factorisation + ////////////////////////////////////////////////////////////////////////// + + // current_minimal_factorisation - no enumeration, returns word_type + m.method("current_minimal_factorisation", + [](FPB const & fpb, uint32_t pos) -> word_type { + return libsemigroups::froidure_pin::current_minimal_factorisation(fpb, pos); + }); + + // minimal_factorisation - triggers enumeration, returns word_type + m.method("minimal_factorisation", [](FPB & fpb, uint32_t pos) -> word_type { + return libsemigroups::froidure_pin::minimal_factorisation(fpb, pos); + }); + + // factorisation - triggers enumeration, returns word_type + m.method("factorisation", [](FPB & fpb, uint32_t pos) -> word_type { + return libsemigroups::froidure_pin::factorisation(fpb, pos); + }); + + ////////////////////////////////////////////////////////////////////////// + // Free functions: position from word + ////////////////////////////////////////////////////////////////////////// + + // current_position_word - no enumeration, returns UNDEFINED if not found + m.method("current_position_word", + [](FPB const & fpb, std::vector const & w) -> uint32_t { + return libsemigroups::froidure_pin::current_position(fpb, w); + }); + + // position_word - triggers full enumeration + m.method("position_word", [](FPB & fpb, std::vector const & w) -> uint32_t { + return libsemigroups::froidure_pin::position(fpb, w); + }); + + ////////////////////////////////////////////////////////////////////////// + // Free functions: rules (two parallel vectors) + ////////////////////////////////////////////////////////////////////////// + + // rules_lhs_vector - full enumeration, returns LHS of all rules + m.method("rules_lhs_vector", [](FPB & fpb) -> std::vector { + std::vector result; + for (auto const & [lhs, rhs] : libsemigroups::froidure_pin::rules(fpb)) + { + result.push_back(lhs); + } + return result; + }); + + // rules_rhs_vector - full enumeration, returns RHS of all rules + m.method("rules_rhs_vector", [](FPB & fpb) -> std::vector { + std::vector result; + for (auto const & [lhs, rhs] : libsemigroups::froidure_pin::rules(fpb)) + { + result.push_back(rhs); + } + return result; + }); + + // current_rules_lhs_vector - no enumeration + m.method("current_rules_lhs_vector", [](FPB const & fpb) -> std::vector { + std::vector result; + for (auto const & [lhs, rhs] : libsemigroups::froidure_pin::current_rules(fpb)) + { + result.push_back(lhs); + } + return result; + }); + + // current_rules_rhs_vector - no enumeration + m.method("current_rules_rhs_vector", [](FPB const & fpb) -> std::vector { + std::vector result; + for (auto const & [lhs, rhs] : libsemigroups::froidure_pin::current_rules(fpb)) + { + result.push_back(rhs); + } + return result; + }); + + ////////////////////////////////////////////////////////////////////////// + // Free functions: normal forms + ////////////////////////////////////////////////////////////////////////// + + // normal_forms_vector - full enumeration + m.method("normal_forms_vector", [](FPB & fpb) -> std::vector { + std::vector result; + for (auto const & w : libsemigroups::froidure_pin::normal_forms(fpb)) + { + result.push_back(w); + } + return result; + }); + + // current_normal_forms_vector - no enumeration + m.method("current_normal_forms_vector", [](FPB const & fpb) -> std::vector { + std::vector result; + for (auto const & w : libsemigroups::froidure_pin::current_normal_forms(fpb)) + { + result.push_back(w); + } + return result; + }); +} + +} // namespace libsemigroups_julia diff --git a/deps/src/froidure-pin.cpp b/deps/src/froidure-pin.cpp new file mode 100644 index 0000000..8184ad1 --- /dev/null +++ b/deps/src/froidure-pin.cpp @@ -0,0 +1,304 @@ +// +// Semigroups.jl +// Copyright (C) 2026, James W. Swent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +#include "libsemigroups_julia.hpp" + +#include +#include + +#include +#include +#include +#include + +// CxxWrap SuperType: enables CxxBaseRef upcasting for all FroidurePin +// types to FroidurePinBase when dispatching inherited methods. +namespace jlcxx { +template struct SuperType> { + typedef libsemigroups::FroidurePinBase type; +}; +} // namespace jlcxx + +namespace libsemigroups_julia { + +namespace { + +template +void bind_froidure_pin(jl::Module & m, + jlcxx::TypeWrapper> & type, + std::string const & type_name) +{ + using FP = libsemigroups::FroidurePin; + using word_type = libsemigroups::word_type; + + ////////////////////////////////////////////////////////////////////////// + // Constructors from individual generators + // StdVector cannot be constructed on the Julia side, + // so we provide individual-element constructors that build the vector + // internally on the C++ side. + ////////////////////////////////////////////////////////////////////////// + + m.method(type_name, [](Element const & g1) -> FP { + std::vector gens = {g1}; + return FP(gens.begin(), gens.end()); + }); + + m.method(type_name, [](Element const & g1, Element const & g2) -> FP { + std::vector gens = {g1, g2}; + return FP(gens.begin(), gens.end()); + }); + + m.method(type_name, + [](Element const & g1, Element const & g2, Element const & g3) -> FP { + std::vector gens = {g1, g2, g3}; + return FP(gens.begin(), gens.end()); + }); + + m.method(type_name, + [](Element const & g1, Element const & g2, Element const & g3, + Element const & g4) -> FP { + std::vector gens = {g1, g2, g3, g4}; + return FP(gens.begin(), gens.end()); + }); + + ////////////////////////////////////////////////////////////////////////// + // Copy + ////////////////////////////////////////////////////////////////////////// + + type.method("copy", [](FP const & self) -> FP { + return FP(self); + }); + + ////////////////////////////////////////////////////////////////////////// + // Generator access + ////////////////////////////////////////////////////////////////////////// + + type.method("number_of_generators", [](FP const & self) -> size_t { + return self.number_of_generators(); + }); + + type.method("generator", [](FP const & self, uint32_t i) -> Element { + return self.generator(i); + }); + + ////////////////////////////////////////////////////////////////////////// + // Element access (return by copy for GC safety) + ////////////////////////////////////////////////////////////////////////// + + type.method("at", [](FP & self, uint32_t i) -> Element { + return self.at(i); + }); + + type.method("sorted_at", [](FP & self, uint32_t i) -> Element { + return self.sorted_at(i); + }); + + ////////////////////////////////////////////////////////////////////////// + // Position / membership (element-based overloads) + // Named with _element suffix to avoid CxxWrap dispatch conflicts with + // FroidurePinBase's index/word-based methods. + ////////////////////////////////////////////////////////////////////////// + + type.method("current_position_element", + [](FP const & self, Element const & x) -> uint32_t { + return self.current_position(x); + }); + + type.method("position_element", [](FP & self, Element const & x) -> uint32_t { + return self.position(x); + }); + + type.method("sorted_position_element", [](FP & self, Element const & x) -> uint32_t { + return self.sorted_position(x); + }); + + type.method("contains_element", [](FP & self, Element const & x) -> bool { + return self.contains(x); + }); + + ////////////////////////////////////////////////////////////////////////// + // Products and index transforms + ////////////////////////////////////////////////////////////////////////// + + type.method("fast_product", [](FP const & self, uint32_t i, uint32_t j) -> uint32_t { + return self.fast_product(i, j); + }); + + type.method("to_sorted_position", [](FP & self, uint32_t i) -> uint32_t { + return self.to_sorted_position(i); + }); + + ////////////////////////////////////////////////////////////////////////// + // Idempotents + ////////////////////////////////////////////////////////////////////////// + + type.method("number_of_idempotents", [](FP & self) -> size_t { + return self.number_of_idempotents(); + }); + + type.method("is_idempotent", [](FP & self, uint32_t i) -> bool { + return self.is_idempotent(i); + }); + + ////////////////////////////////////////////////////////////////////////// + // Generator management (mutating) + ////////////////////////////////////////////////////////////////////////// + + type.method("add_generator!", [](FP & self, Element const & x) { + self.add_generator(x); + }); + + type.method("add_generators!", [](FP & self, std::vector const & gens) { + self.add_generators(gens.begin(), gens.end()); + }); + + type.method("closure!", [](FP & self, std::vector const & gens) { + self.closure(gens.begin(), gens.end()); + }); + + ////////////////////////////////////////////////////////////////////////// + // Copy operations (return new FP) + ////////////////////////////////////////////////////////////////////////// + + type.method("copy_add_generators", + [](FP const & self, std::vector const & gens) -> FP { + return self.copy_add_generators(gens.begin(), gens.end()); + }); + + type.method("copy_closure", [](FP & self, std::vector const & gens) -> FP { + return self.copy_closure(gens.begin(), gens.end()); + }); + + ////////////////////////////////////////////////////////////////////////// + // Reserve + ////////////////////////////////////////////////////////////////////////// + + type.method("reserve!", [](FP & self, size_t val) { + self.reserve(val); + }); + + ////////////////////////////////////////////////////////////////////////// + // Collection methods (collect iterators to vectors) + ////////////////////////////////////////////////////////////////////////// + + type.method("elements_vector", [](FP & self) -> std::vector { + self.run(); + return std::vector(self.cbegin(), self.cend()); + }); + + type.method("sorted_elements_vector", [](FP & self) -> std::vector { + std::vector result; + for (auto const & e : libsemigroups::froidure_pin::sorted_elements(self)) + { + result.push_back(e); + } + return result; + }); + + type.method("idempotents_vector", [](FP & self) -> std::vector { + std::vector result; + for (auto const & e : libsemigroups::froidure_pin::idempotents(self)) + { + result.push_back(e); + } + return result; + }); + + ////////////////////////////////////////////////////////////////////////// + // Free functions: element-dependent + ////////////////////////////////////////////////////////////////////////// + + // to_element: convert word -> Element (uses member function with iterators) + m.method("to_element", [](FP & fp, std::vector const & w) -> Element { + return fp.to_element(w.begin(), w.end()); + }); + + // equal_to_words: check if two words represent the same element + m.method( + "equal_to_words", + [](FP & fp, std::vector const & x, std::vector const & y) -> bool { + return fp.equal_to(x.begin(), x.end(), y.begin(), y.end()); + }); + + // factorisation by element (distinct from FPB's index-based factorisation) + m.method("factorisation_element", [](FP & fp, Element const & x) -> word_type { + return libsemigroups::froidure_pin::factorisation(fp, x); + }); + + m.method("minimal_factorisation_element", [](FP & fp, Element const & x) -> word_type { + return libsemigroups::froidure_pin::minimal_factorisation(fp, x); + }); +} + +} // anonymous namespace + +// Main function to define all FroidurePin template instantiations +void define_froidure_pin(jl::Module & m) +{ + using namespace libsemigroups; + + //////////////////////////////////////////////////////////////////////// + // FroidurePin> + //////////////////////////////////////////////////////////////////////// + + auto fp_transf1 = m.add_type>>( + "FroidurePinTransf1", jlcxx::julia_base_type()); + bind_froidure_pin>(m, fp_transf1, "FroidurePinTransf1"); + + auto fp_transf2 = m.add_type>>( + "FroidurePinTransf2", jlcxx::julia_base_type()); + bind_froidure_pin>(m, fp_transf2, "FroidurePinTransf2"); + + auto fp_transf4 = m.add_type>>( + "FroidurePinTransf4", jlcxx::julia_base_type()); + bind_froidure_pin>(m, fp_transf4, "FroidurePinTransf4"); + + //////////////////////////////////////////////////////////////////////// + // FroidurePin> + //////////////////////////////////////////////////////////////////////// + + auto fp_pperm1 = m.add_type>>( + "FroidurePinPPerm1", jlcxx::julia_base_type()); + bind_froidure_pin>(m, fp_pperm1, "FroidurePinPPerm1"); + + auto fp_pperm2 = m.add_type>>( + "FroidurePinPPerm2", jlcxx::julia_base_type()); + bind_froidure_pin>(m, fp_pperm2, "FroidurePinPPerm2"); + + auto fp_pperm4 = m.add_type>>( + "FroidurePinPPerm4", jlcxx::julia_base_type()); + bind_froidure_pin>(m, fp_pperm4, "FroidurePinPPerm4"); + + //////////////////////////////////////////////////////////////////////// + // FroidurePin> + //////////////////////////////////////////////////////////////////////// + + auto fp_perm1 = m.add_type>>( + "FroidurePinPerm1", jlcxx::julia_base_type()); + bind_froidure_pin>(m, fp_perm1, "FroidurePinPerm1"); + + auto fp_perm2 = m.add_type>>( + "FroidurePinPerm2", jlcxx::julia_base_type()); + bind_froidure_pin>(m, fp_perm2, "FroidurePinPerm2"); + + auto fp_perm4 = m.add_type>>( + "FroidurePinPerm4", jlcxx::julia_base_type()); + bind_froidure_pin>(m, fp_perm4, "FroidurePinPerm4"); +} + +} // namespace libsemigroups_julia diff --git a/deps/src/libsemigroups_julia.cpp b/deps/src/libsemigroups_julia.cpp index baec02d..3a0e329 100644 --- a/deps/src/libsemigroups_julia.cpp +++ b/deps/src/libsemigroups_julia.cpp @@ -28,10 +28,21 @@ JLCXX_MODULE define_julia_module(jl::Module & mod) // Define constants first (UNDEFINED, POSITIVE_INFINITY, etc.) define_constants(mod); + // Define base types (must be registered before derived types) + define_runner(mod); + + // Define WordGraph (must be before FroidurePinBase) + define_word_graph(mod); + + // Define FroidurePinBase (inherits Runner, uses WordGraph) + define_froidure_pin_base(mod); + // Define element types define_transf(mod); - // Add more definitions here (FroidurePin, etc.) + // Define FroidurePin template instantiations + // Must be AFTER transf (element types) AND froidure_pin_base + define_froidure_pin(mod); } } // namespace libsemigroups_julia diff --git a/deps/src/libsemigroups_julia.hpp b/deps/src/libsemigroups_julia.hpp index 7677609..639b310 100644 --- a/deps/src/libsemigroups_julia.hpp +++ b/deps/src/libsemigroups_julia.hpp @@ -22,19 +22,32 @@ #ifndef LIBSEMIGROUPS_JULIA_HPP_ #define LIBSEMIGROUPS_JULIA_HPP_ -// JlCxx headers +// JlCxx headers FIRST — these pull in standard library headers (, +// , etc.) that on libstdc++ use __cpp_lib_is_constant_evaluated to +// decide constexpr-ness. They must be included while the macro is intact. #include "jlcxx/jlcxx.hpp" #include "jlcxx/stl.hpp" -// libsemigroups headers +// FIX for fmt consteval issue: +// JlCxx requires C++20, but libsemigroups bundles fmt which enables +// consteval format string validation in C++20 mode, causing compile errors +// when inline functions pass runtime std::string_view to fmt::format. +// +// fmt/base.h unconditionally defines FMT_USE_CONSTEVAL via an #if/#elif +// chain (no #ifndef guard), so pre-defining it has no effect. Instead we +// #undef __cpp_lib_is_constant_evaluated AFTER all standard library headers +// are included (so their constexpr declarations are correct) but BEFORE any +// libsemigroups/fmt headers. When fmt/base.h later includes , +// include guards prevent re-parsing, so the macro stays undefined and fmt +// takes the !defined(__cpp_lib_is_constant_evaluated) branch, setting +// FMT_USE_CONSTEVAL=0. +#undef __cpp_lib_is_constant_evaluated + +// libsemigroups headers (these transitively include fmt) #include +#include #include - -// Standard library -#include -#include -#include -#include +#include namespace libsemigroups_julia { @@ -44,7 +57,11 @@ namespace libsemigroups = ::libsemigroups; // Forward declarations of binding functions void define_constants(jl::Module & mod); +void define_runner(jl::Module & mod); +void define_word_graph(jl::Module & mod); +void define_froidure_pin_base(jl::Module & mod); void define_transf(jl::Module & mod); +void define_froidure_pin(jl::Module & mod); } // namespace libsemigroups_julia diff --git a/deps/src/runner.cpp b/deps/src/runner.cpp new file mode 100644 index 0000000..122597e --- /dev/null +++ b/deps/src/runner.cpp @@ -0,0 +1,156 @@ +// runner.cpp - Runner base class 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 Runner class to Julia via CxxWrap. +// Runner is an abstract base class providing algorithm execution control +// (run, run_for, timeout, stop, etc.) used by FroidurePinBase and other +// algorithm classes. + +#include "libsemigroups_julia.hpp" + +#include + +#include + +#include +#include +#include + +namespace libsemigroups_julia { + +void define_runner(jl::Module & m) +{ + using libsemigroups::Runner; + + // Register Runner as a base type. + // Runner is abstract (pure virtual run_impl/finished_impl) so we do NOT + // add constructors. It will only be usable through derived types + // (e.g. FroidurePinBase). + auto type = m.add_type("Runner"); + + ////////////////////////////////////////////////////////////////////////// + // State enum + ////////////////////////////////////////////////////////////////////////// + + m.add_bits("state", jl::julia_type("CppEnum")); + m.set_const("state_never_run", Runner::state::never_run); + m.set_const("state_running_to_finish", Runner::state::running_to_finish); + m.set_const("state_running_for", Runner::state::running_for); + m.set_const("state_running_until", Runner::state::running_until); + m.set_const("state_timed_out", Runner::state::timed_out); + m.set_const("state_stopped_by_predicate", Runner::state::stopped_by_predicate); + m.set_const("state_not_running", Runner::state::not_running); + m.set_const("state_dead", Runner::state::dead); + + ////////////////////////////////////////////////////////////////////////// + // Core algorithm control + ////////////////////////////////////////////////////////////////////////// + + // run / run! - Run the algorithm to completion + type.method("run!", [](Runner & self) { + self.run(); + }); + + // run_for / run_for! - Run for a specified duration. + // We accept Int64 nanoseconds from Julia (the Julia layer converts + // Dates.TimePeriod to nanoseconds before calling this binding). + type.method("run_for!", [](Runner & self, int64_t ns) { + self.run_for(std::chrono::nanoseconds(ns)); + }); + + // run_until / run_until! - Run until a nullary predicate returns true. + // CxxWrap does not support std::function as a parameter type, so we + // accept a SafeCFunction and convert it to a function pointer via + // make_function_pointer. The Julia side uses @safe_cfunction to create the + // SafeCFunction from a closure. A uint8_t is used instead of bool to + // avoid C++ bool ABI issues across platforms. + type.method("run_until!", [](Runner & self, jlcxx::SafeCFunction func) { + auto fp = jlcxx::make_function_pointer(func); + self.run_until([fp]() -> bool { + return fp() != 0; + }); + }); + + // init - Re-initialize the runner to its default-constructed state + type.method("init!", [](Runner & self) -> Runner & { + return self.init(); + }); + + ////////////////////////////////////////////////////////////////////////// + // State queries + ////////////////////////////////////////////////////////////////////////// + + // finished - Has the algorithm run to completion? + type.method("finished", &Runner::finished); + + // success - Has the algorithm completed successfully? + type.method("success", &Runner::success); + + // started - Has run() been called at least once? + type.method("started", &Runner::started); + + // running - Is the algorithm currently executing? + type.method("running", &Runner::running); + + // timed_out - Did run_for! exhaust its time limit? + type.method("timed_out", &Runner::timed_out); + + // stopped - Is the algorithm stopped for any reason? + // (finished, timed_out, dead, or stopped_by_predicate) + type.method("stopped", &Runner::stopped); + + // dead - Was the runner killed from another thread? + type.method("dead", &Runner::dead); + + // stopped_by_predicate - Was run_until's predicate satisfied? + type.method("stopped_by_predicate", &Runner::stopped_by_predicate); + + // running_for - Is it currently in a run_for! call? + type.method("running_for", &Runner::running_for); + + // running_for_how_long - Return last run_for duration in nanoseconds + type.method("running_for_how_long", [](Runner const & self) -> int64_t { + return self.running_for_how_long().count(); + }); + + // running_until - Is it currently in a run_until call? + type.method("running_until", &Runner::running_until); + + // current_state - Return the current state enum value + type.method("current_state", &Runner::current_state); + + ////////////////////////////////////////////////////////////////////////// + // Control + ////////////////////////////////////////////////////////////////////////// + + // kill / kill! - Stop the runner from another thread (thread-safe) + type.method("kill!", [](Runner & self) { + self.kill(); + }); + + ////////////////////////////////////////////////////////////////////////// + // Reporting + ////////////////////////////////////////////////////////////////////////// + + // report_why_we_stopped - Print reason for stopping to stderr + type.method("report_why_we_stopped", &Runner::report_why_we_stopped); + + // string_why_we_stopped - Return reason for stopping as a string + type.method("string_why_we_stopped", &Runner::string_why_we_stopped); +} + +} // namespace libsemigroups_julia diff --git a/deps/src/transf.cpp b/deps/src/transf.cpp index 920dc9c..fa07900 100644 --- a/deps/src/transf.cpp +++ b/deps/src/transf.cpp @@ -122,8 +122,8 @@ void bind_ptransf_common(jl::Module & m, type.method("is_greater_equal", [](PTransfType const & a, PTransfType const & b) -> bool { - return a >= b; - }); + return a >= b; + }); type.method("multiply", [](PTransfType const & a, PTransfType const & b) { return a * b; @@ -154,8 +154,8 @@ void bind_pperm_type(jl::Module & m, std::string const & name) m.method(name, [](std::vector const & dom, std::vector const & img, size_t deg) -> PPermType { - return libsemigroups::make(dom, img, deg); - }); + return libsemigroups::make(dom, img, deg); + }); } template void bind_perm_type(jl::Module & m, std::string const & name) diff --git a/deps/src/word-graph.cpp b/deps/src/word-graph.cpp new file mode 100644 index 0000000..dbfdbb3 --- /dev/null +++ b/deps/src/word-graph.cpp @@ -0,0 +1,128 @@ +// +// Semigroups.jl +// Copyright (C) 2026, James W. Swent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "libsemigroups_julia.hpp" + +#include +#include +#include +#include + +namespace libsemigroups_julia { + +void define_word_graph(jl::Module & m) +{ + using WG = libsemigroups::WordGraph; + + auto type = m.add_type("WordGraph"); + + ////////////////////////////////////////////////////////////////////////// + // Constructor + ////////////////////////////////////////////////////////////////////////// + + m.method("WordGraph", [](size_t num_nodes, size_t out_deg) -> WG { + return WG(num_nodes, out_deg); + }); + + ////////////////////////////////////////////////////////////////////////// + // Size / structure queries + ////////////////////////////////////////////////////////////////////////// + + // number_of_nodes - total number of nodes in the graph + type.method("number_of_nodes", &WG::number_of_nodes); + + // out_degree - number of edge labels (same for all nodes) + type.method("out_degree", &WG::out_degree); + + // number_of_edges - total number of defined edges (all nodes) + type.method("number_of_edges", [](WG const & self) -> size_t { + return self.number_of_edges(); + }); + + // number_of_edges_node - number of defined edges from a specific node + type.method("number_of_edges_node", [](WG const & self, uint32_t s) -> size_t { + return self.number_of_edges(s); + }); + + ////////////////////////////////////////////////////////////////////////// + // Edge lookup + ////////////////////////////////////////////////////////////////////////// + + // target - get the target of edge (source, label). + // Returns UNDEFINED (as uint32_t max) if no such edge is defined. + type.method("target", [](WG const & self, uint32_t source, uint32_t label) -> uint32_t { + return self.target(source, label); + }); + + // next_label_and_target - find next defined edge from node s with label >= a. + // Split into two methods to avoid std::pair which CxxWrap can't return. + // next_label: returns the label of the next defined edge (UNDEFINED if none) + // next_target: returns the target of the next defined edge (UNDEFINED if none) + type.method("next_label", [](WG const & self, uint32_t s, uint32_t a) -> uint32_t { + return self.next_label_and_target(s, a).first; + }); + type.method("next_target", [](WG const & self, uint32_t s, uint32_t a) -> uint32_t { + return self.next_label_and_target(s, a).second; + }); + + ////////////////////////////////////////////////////////////////////////// + // Iteration helpers (collect to vector for CxxWrap) + ////////////////////////////////////////////////////////////////////////// + + // targets_vector - all targets from a given source node as a vector. + // Includes UNDEFINED entries for labels with no defined edge. + type.method( + "targets_vector", [](WG const & self, uint32_t source) -> std::vector { + std::vector result; + result.reserve(self.out_degree()); + for (auto it = self.cbegin_targets(source); it != self.cend_targets(source); ++it) + { + result.push_back(*it); + } + return result; + }); + + ////////////////////////////////////////////////////////////////////////// + // Comparison + ////////////////////////////////////////////////////////////////////////// + + type.method("is_equal", [](WG const & a, WG const & b) -> bool { + return a == b; + }); + type.method("is_not_equal", [](WG const & a, WG const & b) -> bool { + return a != b; + }); + type.method("is_less", [](WG const & a, WG const & b) -> bool { + return a < b; + }); + + ////////////////////////////////////////////////////////////////////////// + // Copy and hash + ////////////////////////////////////////////////////////////////////////// + + // copy - returns a deep copy + type.method("copy", [](WG const & self) -> WG { + return WG(self); + }); + + // hash - for use in Julia Base.hash + type.method("hash", [](WG const & self) -> size_t { + return self.hash_value(); + }); +} + +} // namespace libsemigroups_julia diff --git a/src/Semigroups.jl b/src/Semigroups.jl index 267372e..ce27c36 100644 --- a/src/Semigroups.jl +++ b/src/Semigroups.jl @@ -8,6 +8,7 @@ module Semigroups using CxxWrap using AbstractAlgebra +using Dates: TimePeriod, Nanosecond # ============================================================================ # Debug mode @@ -60,11 +61,19 @@ using .Errors: LibsemigroupsError, @wrap_libsemigroups_call # Julia-side wrapper files include("libsemigroups/constants.jl") +include("libsemigroups/runner.jl") +include("libsemigroups/word-graph.jl") include("libsemigroups/transf.jl") +# Type alias for FroidurePinBase (abstract base; full API comes with FroidurePin{E}) +const FroidurePinBase = LibSemigroups.FroidurePinBase + # High-level element types include("elements/transf.jl") +# High-level FroidurePin API +include("froidure-pin.jl") + # Module initialization function __init__() # Initialize the CxxWrap module @@ -77,10 +86,36 @@ end export enable_debug, is_debug, LibsemigroupsError export UNDEFINED, POSITIVE_INFINITY, NEGATIVE_INFINITY, LIMIT_MAX +export Runner, RunnerState +export STATE_NEVER_RUN, STATE_RUNNING_TO_FINISH, STATE_RUNNING_FOR +export STATE_RUNNING_UNTIL, STATE_TIMED_OUT, STATE_STOPPED_BY_PREDICATE +export STATE_NOT_RUNNING, STATE_DEAD +export run!, run_for!, run_until!, init!, kill! +export finished, started, running, timed_out, stopped, dead +export stopped_by_predicate, running_for, running_until +export current_state, running_for_how_long +export report_why_we_stopped, string_why_we_stopped export tril, tril_FALSE, tril_TRUE, tril_unknown, tril_to_bool export is_undefined, is_positive_infinity, is_negative_infinity, is_limit_max +# WordGraph +export WordGraph +export number_of_nodes, out_degree, number_of_edges + +# FroidurePinBase +export FroidurePinBase + # Transformation types and functions +export FroidurePin +export number_of_generators, generator, generators, current_size +export sorted_position, sorted_at, to_sorted_position +export fast_product, number_of_idempotents, is_idempotent, idempotents +export factorisation, minimal_factorisation, to_element, equal_to +export prefix, suffix, first_letter, final_letter, word_length, current_max_word_length +export elements, sorted_elements +export number_of_rules, contains_one +export right_cayley_graph, left_cayley_graph +export batch_size, set_batch_size!, reserve!, add_generator! export Transf, PPerm, Perm export degree, rank, images, image_set, domain_set export increase_degree_by!, swap! diff --git a/src/froidure-pin.jl b/src/froidure-pin.jl new file mode 100644 index 0000000..7d6c308 --- /dev/null +++ b/src/froidure-pin.jl @@ -0,0 +1,636 @@ +# 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. + +""" +froidure-pin.jl - High-level Julia API for FroidurePin + +This file provides the user-facing `FroidurePin{E}` parametric type wrapping +the C++ FroidurePin template instantiations. All indices are 1-based +following Julia conventions. +""" + +using CxxWrap.StdLib: StdVector +using CxxWrap.CxxWrapCore: CxxULong + +# ============================================================================ +# Helper functions (private) +# ============================================================================ + +# Map from CxxWrap element type → FroidurePin constructor function +const _FP_CONSTRUCTOR_MAP = Dict{DataType,Any}( + Transf1 => LibSemigroups.FroidurePinTransf1, + Transf2 => LibSemigroups.FroidurePinTransf2, + Transf4 => LibSemigroups.FroidurePinTransf4, + PPerm1 => LibSemigroups.FroidurePinPPerm1, + PPerm2 => LibSemigroups.FroidurePinPPerm2, + PPerm4 => LibSemigroups.FroidurePinPPerm4, + Perm1 => LibSemigroups.FroidurePinPerm1, + Perm2 => LibSemigroups.FroidurePinPerm2, + Perm4 => LibSemigroups.FroidurePinPerm4, +) + +# Map Julia element type → CxxWrap FroidurePin constructor +function _cxx_fp_constructor(::Type{Transf{T}}) where {T} + return _FP_CONSTRUCTOR_MAP[_transf_type_from_scalar_type(T)] +end + +function _cxx_fp_constructor(::Type{PPerm{T}}) where {T} + return _FP_CONSTRUCTOR_MAP[_pperm_type_from_scalar_type(T)] +end + +function _cxx_fp_constructor(::Type{Perm{T}}) where {T} + return _FP_CONSTRUCTOR_MAP[_perm_type_from_scalar_type(T)] +end + +# Dispatch to 1/2/3/4-arg C++ constructors, or fallback for >4 +function _construct_cxx_fp(Constructor, cxx_gens::Vector) + n = length(cxx_gens) + if n == 1 + return Constructor(cxx_gens[1]) + elseif n == 2 + return Constructor(cxx_gens[1], cxx_gens[2]) + elseif n == 3 + return Constructor(cxx_gens[1], cxx_gens[2], cxx_gens[3]) + elseif n == 4 + return Constructor(cxx_gens[1], cxx_gens[2], cxx_gens[3], cxx_gens[4]) + else + # >4: construct with first, then add_generator! for rest + fp = Constructor(cxx_gens[1]) + for i in 2:n + LibSemigroups.add_generator!(fp, cxx_gens[i]) + end + return fp + end +end + +# Wrap C++ element back to high-level Julia type. +# Uses Transf()/PPerm()/Perm() constructors which accept any CxxWrap variant +# (Allocated or Dereferenced). For Dereferenced elements (from C++ vectors), +# these constructors create owned copies via CxxWrap's implicit conversion. +_wrap_element(::Type{Transf{T}}, cxx_elem) where {T} = Transf(LibSemigroups.copy(cxx_elem)) +_wrap_element(::Type{PPerm{T}}, cxx_elem) where {T} = PPerm(LibSemigroups.copy(cxx_elem)) +_wrap_element(::Type{Perm{T}}, cxx_elem) where {T} = Perm(LibSemigroups.copy(cxx_elem)) + +# Word conversion (0-based C++ <-> 1-based Julia) +_to_word_1based(cxx_word) = [Int(w) + 1 for w in cxx_word] +_to_word_0based(word) = StdVector{CxxULong}(CxxULong[w - 1 for w in word]) + +# Check if C++ returned UNDEFINED and convert to nothing (with +1 shift) +_maybe_undefined(val::UInt32) = is_undefined(val, UInt32) ? nothing : Int(val) + 1 + +# ============================================================================ +# FroidurePin{E} type +# ============================================================================ + +""" + FroidurePin{E} + +A semigroup defined by generators of element type `E`, enumerated using the +Froidure-Pin algorithm. `E` is one of `Transf{T}`, `PPerm{T}`, or `Perm{T}`. + +All indices are 1-based (Julia convention). The semigroup is lazily enumerated: +elements are computed on demand when queried. + +# Construction +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) # S3 +S = FroidurePin([Transf([2, 1, 3]), Transf([2, 3, 1])]) # same, from vector +``` +""" +mutable struct FroidurePin{E} + cxx_obj::Any # CxxWrap FroidurePinTransf1Allocated, etc. +end + +# ============================================================================ +# Constructors +# ============================================================================ + +""" + FroidurePin(gens::AbstractVector{E}) where {E} + +Construct a FroidurePin semigroup from a vector of generators. +Requires at least one generator. + +# Example +```julia +S = FroidurePin([Transf([2, 1, 3]), Transf([2, 3, 1])]) +``` +""" +function FroidurePin(gens::AbstractVector{E}) where {E<:Union{Transf,PPerm,Perm}} + isempty(gens) && throw(ArgumentError("at least one generator required")) + Constructor = _cxx_fp_constructor(E) + cxx_gens = [g.cxx_obj for g in gens] + cxx_obj = _construct_cxx_fp(Constructor, cxx_gens) + return FroidurePin{E}(cxx_obj) +end + +""" + FroidurePin(g1::E, gs::E...) where {E} + +Construct a FroidurePin semigroup from one or more generators (varargs). + +# Example +```julia +S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1])) +``` +""" +FroidurePin(g1::E, gs::E...) where {E<:Union{Transf,PPerm,Perm}} = + FroidurePin(collect(E, (g1, gs...))) + +# ============================================================================ +# Runner delegation +# ============================================================================ + +""" + run!(S::FroidurePin) -> FroidurePin + +Run the Froidure-Pin algorithm to completion. Returns `S` for method chaining. +""" +run!(S::FroidurePin) = (LibSemigroups.run!(S.cxx_obj); S) + +""" + run_for!(S::FroidurePin, t::TimePeriod) -> FroidurePin + +Run the algorithm for at most duration `t`. Returns `S` for method chaining. +""" +run_for!(S::FroidurePin, t::TimePeriod) = (run_for!(S.cxx_obj, t); S) + +""" + finished(S::FroidurePin) -> Bool + +Return `true` if the semigroup has been fully enumerated. +""" +finished(S::FroidurePin) = LibSemigroups.finished(S.cxx_obj) + +""" + started(S::FroidurePin) -> Bool + +Return `true` if enumeration has been started. +""" +started(S::FroidurePin) = LibSemigroups.started(S.cxx_obj) + +""" + timed_out(S::FroidurePin) -> Bool + +Return `true` if the last `run_for!` call timed out. +""" +timed_out(S::FroidurePin) = LibSemigroups.timed_out(S.cxx_obj) + +# ============================================================================ +# Size and degree +# ============================================================================ + +""" + length(S::FroidurePin) -> Int + +Return the number of elements in the semigroup. Triggers full enumeration. +""" +Base.length(S::FroidurePin) = Int(LibSemigroups.size(S.cxx_obj)) + +""" + current_size(S::FroidurePin) -> Int + +Return the number of elements enumerated so far (no further enumeration). +""" +current_size(S::FroidurePin) = Int(LibSemigroups.current_size(S.cxx_obj)) + +""" + degree(S::FroidurePin) -> Int + +Return the degree of the elements in the semigroup. +""" +degree(S::FroidurePin) = Int(LibSemigroups.degree(S.cxx_obj)) + +# ============================================================================ +# Generators +# ============================================================================ + +""" + number_of_generators(S::FroidurePin) -> Int + +Return the number of generators of the semigroup. +""" +number_of_generators(S::FroidurePin) = Int(LibSemigroups.number_of_generators(S.cxx_obj)) + +""" + generator(S::FroidurePin{E}, i::Integer) where E -> E + +Return the `i`-th generator (1-based indexing). +""" +function generator(S::FroidurePin{E}, i::Integer) where {E} + (i < 1 || i > number_of_generators(S)) && throw(BoundsError(S, i)) + return _wrap_element(E, LibSemigroups.generator(S.cxx_obj, UInt32(i - 1))) +end + +""" + generators(S::FroidurePin) -> Vector + +Return a vector of all generators. +""" +generators(S::FroidurePin) = [generator(S, i) for i in 1:number_of_generators(S)] + +# ============================================================================ +# Collection protocol +# ============================================================================ + +""" + getindex(S::FroidurePin{E}, i::Integer) where E -> E + +Return the `i`-th element (1-based). Triggers full enumeration. +""" +function Base.getindex(S::FroidurePin{E}, i::Integer) where {E} + (i < 1 || i > length(S)) && throw(BoundsError(S, i)) + return _wrap_element(E, LibSemigroups.at(S.cxx_obj, UInt32(i - 1))) +end + +""" + iterate(S::FroidurePin[, state]) -> Union{Tuple, Nothing} + +Iterate over elements of the semigroup. Triggers full enumeration. +""" +function Base.iterate(S::FroidurePin, state = 1) + state > length(S) && return nothing + return (S[state], state + 1) +end + +""" + in(x::E, S::FroidurePin{E}) -> Bool + +Return `true` if element `x` is in the semigroup `S`. +""" +Base.in(x::E, S::FroidurePin{E}) where {E<:Union{Transf,PPerm,Perm}} = + LibSemigroups.contains_element(S.cxx_obj, x.cxx_obj) + +""" + eltype(::Type{FroidurePin{E}}) -> Type + +Return the element type of the semigroup. +""" +Base.eltype(::Type{FroidurePin{E}}) where {E} = E + +""" + copy(S::FroidurePin{E}) -> FroidurePin{E} + +Return an independent copy of the semigroup. +""" +Base.copy(S::FroidurePin{E}) where {E} = FroidurePin{E}(LibSemigroups.copy(S.cxx_obj)) + +# ============================================================================ +# Display +# ============================================================================ + +function Base.show(io::IO, S::FroidurePin{E}) where {E} + n = number_of_generators(S) + if finished(S) + print(io, "") + elseif started(S) + print(io, "") + else + print(io, "") + end +end + +# ============================================================================ +# Position / membership +# ============================================================================ + +""" + position(S::FroidurePin{E}, x::E) -> Union{Int, Nothing} + +Return the 1-based position of `x` in `S`, or `nothing` if not found. +Triggers full enumeration. +""" +function position(S::FroidurePin{E}, x::E) where {E} + pos = LibSemigroups.position_element(S.cxx_obj, x.cxx_obj) + return _maybe_undefined(pos) +end + +""" + current_position(S::FroidurePin{E}, x::E) -> Union{Int, Nothing} + +Return the 1-based position of `x` among elements enumerated so far, +or `nothing` if not yet found. Does not trigger further enumeration. +""" +function current_position(S::FroidurePin{E}, x::E) where {E} + pos = LibSemigroups.current_position_element(S.cxx_obj, x.cxx_obj) + return _maybe_undefined(pos) +end + +""" + sorted_position(S::FroidurePin{E}, x::E) -> Union{Int, Nothing} + +Return the 1-based position of `x` in the sorted enumeration order, +or `nothing` if not found. Triggers full enumeration. +""" +function sorted_position(S::FroidurePin{E}, x::E) where {E} + pos = LibSemigroups.sorted_position_element(S.cxx_obj, x.cxx_obj) + return _maybe_undefined(pos) +end + +""" + sorted_at(S::FroidurePin{E}, i::Integer) -> E + +Return the `i`-th element in sorted order (1-based). Triggers full enumeration. +""" +function sorted_at(S::FroidurePin{E}, i::Integer) where {E} + (i < 1 || i > length(S)) && throw(BoundsError(S, i)) + return _wrap_element(E, LibSemigroups.sorted_at(S.cxx_obj, UInt32(i - 1))) +end + +""" + to_sorted_position(S::FroidurePin, i::Integer) -> Int + +Convert a 1-based enumeration-order position to a 1-based sorted-order position. +""" +function to_sorted_position(S::FroidurePin, i::Integer) + (i < 1 || i > length(S)) && throw(BoundsError(S, i)) + return Int(LibSemigroups.to_sorted_position(S.cxx_obj, UInt32(i - 1))) + 1 +end + +# ============================================================================ +# Products +# ============================================================================ + +""" + fast_product(S::FroidurePin, i::Integer, j::Integer) -> Int + +Return the 1-based position of S[i] * S[j]. Both `i` and `j` are 1-based. +The semigroup must be fully enumerated. +""" +function fast_product(S::FroidurePin, i::Integer, j::Integer) + return Int(LibSemigroups.fast_product(S.cxx_obj, UInt32(i - 1), UInt32(j - 1))) + 1 +end + +# ============================================================================ +# Idempotents +# ============================================================================ + +""" + number_of_idempotents(S::FroidurePin) -> Int + +Return the number of idempotent elements. Triggers full enumeration. +""" +number_of_idempotents(S::FroidurePin) = Int(LibSemigroups.number_of_idempotents(S.cxx_obj)) + +""" + is_idempotent(S::FroidurePin, i::Integer) -> Bool + +Return `true` if the element at 1-based position `i` is idempotent. +""" +function is_idempotent(S::FroidurePin, i::Integer) + (i < 1 || i > length(S)) && throw(BoundsError(S, i)) + return LibSemigroups.is_idempotent(S.cxx_obj, UInt32(i - 1)) +end + +""" + idempotents(S::FroidurePin{E}) -> Vector{E} + +Return a vector of all idempotent elements. +""" +function idempotents(S::FroidurePin{E}) where {E} + cxx_vec = LibSemigroups.idempotents_vector(S.cxx_obj) + return [_wrap_element(E, e) for e in cxx_vec] +end + +# ============================================================================ +# Factorisation +# ============================================================================ + +""" + factorisation(S::FroidurePin, i::Integer) -> Vector{Int} + +Return the factorisation of the element at 1-based position `i` as a +1-based word over generator indices. Triggers full enumeration. +""" +function factorisation(S::FroidurePin, i::Integer) + (i < 1 || i > length(S)) && throw(BoundsError(S, i)) + return _to_word_1based(LibSemigroups.factorisation(S.cxx_obj, UInt32(i - 1))) +end + +""" + factorisation(S::FroidurePin{E}, x::E) -> Vector{Int} + +Return the factorisation of element `x` as a 1-based word over generator indices. +""" +function factorisation(S::FroidurePin{E}, x::E) where {E} + return _to_word_1based(LibSemigroups.factorisation_element(S.cxx_obj, x.cxx_obj)) +end + +""" + minimal_factorisation(S::FroidurePin, i::Integer) -> Vector{Int} + +Return the minimal (short-lex least) factorisation of the element at 1-based +position `i` as a 1-based word over generator indices. +""" +function minimal_factorisation(S::FroidurePin, i::Integer) + (i < 1 || i > length(S)) && throw(BoundsError(S, i)) + return _to_word_1based(LibSemigroups.minimal_factorisation(S.cxx_obj, UInt32(i - 1))) +end + +""" + minimal_factorisation(S::FroidurePin{E}, x::E) -> Vector{Int} + +Return the minimal factorisation of element `x` as a 1-based word. +""" +function minimal_factorisation(S::FroidurePin{E}, x::E) where {E} + return _to_word_1based(LibSemigroups.minimal_factorisation_element(S.cxx_obj, x.cxx_obj)) +end + +# ============================================================================ +# Word operations +# ============================================================================ + +""" + to_element(S::FroidurePin{E}, word::AbstractVector{<:Integer}) -> E + +Convert a 1-based word (vector of generator indices) to the corresponding element. +""" +function to_element(S::FroidurePin{E}, word::AbstractVector{<:Integer}) where {E} + cxx_word = _to_word_0based(word) + return _wrap_element(E, LibSemigroups.to_element(S.cxx_obj, cxx_word)) +end + +""" + equal_to(S::FroidurePin, w1, w2) -> Bool + +Return `true` if 1-based words `w1` and `w2` represent the same element. +""" +function equal_to(S::FroidurePin, w1::AbstractVector{<:Integer}, w2::AbstractVector{<:Integer}) + return LibSemigroups.equal_to_words(S.cxx_obj, _to_word_0based(w1), _to_word_0based(w2)) +end + +# ============================================================================ +# Structure (prefix, suffix, first/final letter, word length) +# ============================================================================ + +""" + prefix(S::FroidurePin, i::Integer) -> Union{Int, Nothing} + +Return the 1-based position of the prefix of the element at position `i`, +or `nothing` for generators (which have no proper prefix). +""" +function prefix(S::FroidurePin, i::Integer) + (i < 1 || i > length(S)) && throw(BoundsError(S, i)) + return _maybe_undefined(LibSemigroups.prefix(S.cxx_obj, UInt32(i - 1))) +end + +""" + suffix(S::FroidurePin, i::Integer) -> Union{Int, Nothing} + +Return the 1-based position of the suffix of the element at position `i`, +or `nothing` for generators. +""" +function suffix(S::FroidurePin, i::Integer) + (i < 1 || i > length(S)) && throw(BoundsError(S, i)) + return _maybe_undefined(LibSemigroups.suffix(S.cxx_obj, UInt32(i - 1))) +end + +""" + first_letter(S::FroidurePin, i::Integer) -> Int + +Return the 1-based index of the first generator in the factorisation +of the element at position `i`. +""" +function first_letter(S::FroidurePin, i::Integer) + (i < 1 || i > length(S)) && throw(BoundsError(S, i)) + return Int(LibSemigroups.first_letter(S.cxx_obj, UInt32(i - 1))) + 1 +end + +""" + final_letter(S::FroidurePin, i::Integer) -> Int + +Return the 1-based index of the last generator in the factorisation +of the element at position `i`. +""" +function final_letter(S::FroidurePin, i::Integer) + (i < 1 || i > length(S)) && throw(BoundsError(S, i)) + return Int(LibSemigroups.final_letter(S.cxx_obj, UInt32(i - 1))) + 1 +end + +""" + word_length(S::FroidurePin, i::Integer) -> Int + +Return the length of the factorisation of the element at 1-based position `i`. +""" +function word_length(S::FroidurePin, i::Integer) + (i < 1 || i > length(S)) && throw(BoundsError(S, i)) + return Int(LibSemigroups.length(S.cxx_obj, UInt32(i - 1))) +end + +""" + current_max_word_length(S::FroidurePin) -> Int + +Return the maximum word length among elements enumerated so far. +""" +current_max_word_length(S::FroidurePin) = Int(LibSemigroups.current_max_word_length(S.cxx_obj)) + +# ============================================================================ +# Bulk element access +# ============================================================================ + +""" + elements(S::FroidurePin{E}) -> Vector{E} + +Return a vector of all elements. Triggers full enumeration. +""" +function elements(S::FroidurePin{E}) where {E} + return [_wrap_element(E, e) for e in LibSemigroups.elements_vector(S.cxx_obj)] +end + +""" + sorted_elements(S::FroidurePin{E}) -> Vector{E} + +Return a vector of all elements in sorted order. +""" +function sorted_elements(S::FroidurePin{E}) where {E} + return [_wrap_element(E, e) for e in LibSemigroups.sorted_elements_vector(S.cxx_obj)] +end + +# ============================================================================ +# Rules +# ============================================================================ + +""" + number_of_rules(S::FroidurePin) -> Int + +Return the total number of rules (relations) in the semigroup. +Triggers full enumeration. +""" +number_of_rules(S::FroidurePin) = Int(LibSemigroups.number_of_rules(S.cxx_obj)) + +""" + contains_one(S::FroidurePin) -> Bool + +Return `true` if the semigroup contains the identity element. +Triggers full enumeration. +""" +contains_one(S::FroidurePin) = LibSemigroups.contains_one(S.cxx_obj) + +# ============================================================================ +# Cayley graphs +# ============================================================================ + +""" + right_cayley_graph(S::FroidurePin) -> WordGraph + +Return the right Cayley graph of the semigroup. Triggers full enumeration. +""" +right_cayley_graph(S::FroidurePin) = LibSemigroups.right_cayley_graph(S.cxx_obj) + +""" + left_cayley_graph(S::FroidurePin) -> WordGraph + +Return the left Cayley graph of the semigroup. Triggers full enumeration. +""" +left_cayley_graph(S::FroidurePin) = LibSemigroups.left_cayley_graph(S.cxx_obj) + +# ============================================================================ +# Settings +# ============================================================================ + +""" + batch_size(S::FroidurePin) -> Int + +Return the current batch size used during enumeration. +""" +batch_size(S::FroidurePin) = Int(LibSemigroups.batch_size(S.cxx_obj)) + +""" + set_batch_size!(S::FroidurePin, n::Integer) -> FroidurePin + +Set the batch size for enumeration. Returns `S` for method chaining. +""" +function set_batch_size!(S::FroidurePin, n::Integer) + n < 1 && throw(ArgumentError("batch_size must be positive, got $n")) + LibSemigroups.set_batch_size!(S.cxx_obj, n) + return S +end + +""" + reserve!(S::FroidurePin, n::Integer) -> FroidurePin + +Reserve capacity for `n` elements. Returns `S` for method chaining. +""" +function reserve!(S::FroidurePin, n::Integer) + n < 0 && throw(ArgumentError("reserve size must be non-negative, got $n")) + LibSemigroups.reserve!(S.cxx_obj, n) + return S +end + +# ============================================================================ +# Mutating operations +# ============================================================================ + +""" + add_generator!(S::FroidurePin{E}, x::E) -> FroidurePin{E} + +Add a generator to the semigroup. Returns `S` for method chaining. +The semigroup must not have been fully enumerated yet. +""" +function add_generator!(S::FroidurePin{E}, x::E) where {E} + LibSemigroups.add_generator!(S.cxx_obj, x.cxx_obj) + return S +end diff --git a/src/libsemigroups/runner.jl b/src/libsemigroups/runner.jl new file mode 100644 index 0000000..731d565 --- /dev/null +++ b/src/libsemigroups/runner.jl @@ -0,0 +1,240 @@ +# 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. + +""" +runner.jl - Julia wrappers for libsemigroups Runner base class + +This file provides low-level Julia wrappers for the C++ Runner class exposed +via CxxWrap. Runner is an abstract base class providing algorithm execution +control (run, run_for, timeout, stop, etc.) used by FroidurePinBase and other +algorithm classes. +""" + +# ============================================================================ +# Type aliases +# ============================================================================ + +""" + Runner + +Abstract base type for algorithm runners in libsemigroups. This type is not +directly constructible; it is used as the base type for concrete algorithm +classes such as `FroidurePinBase`. +""" +const Runner = LibSemigroups.Runner + +""" + RunnerState + +Enum type for the state of a [`Runner`](@ref). Possible values: + +- `STATE_NEVER_RUN` - the runner has never been run +- `STATE_RUNNING_TO_FINISH` - running to completion +- `STATE_RUNNING_FOR` - running for a bounded duration +- `STATE_RUNNING_UNTIL` - running until a predicate is satisfied +- `STATE_TIMED_OUT` - the last `run_for!` call timed out +- `STATE_STOPPED_BY_PREDICATE` - the last `run_until` call was stopped +- `STATE_NOT_RUNNING` - the runner is not currently running +- `STATE_DEAD` - the runner was killed +""" +const RunnerState = LibSemigroups.state + +const STATE_NEVER_RUN = LibSemigroups.state_never_run +const STATE_RUNNING_TO_FINISH = LibSemigroups.state_running_to_finish +const STATE_RUNNING_FOR = LibSemigroups.state_running_for +const STATE_RUNNING_UNTIL = LibSemigroups.state_running_until +const STATE_TIMED_OUT = LibSemigroups.state_timed_out +const STATE_STOPPED_BY_PREDICATE = LibSemigroups.state_stopped_by_predicate +const STATE_NOT_RUNNING = LibSemigroups.state_not_running +const STATE_DEAD = LibSemigroups.state_dead + +# ============================================================================ +# Core algorithm control +# ============================================================================ + +""" + run!(r::Runner) + +Run the algorithm to completion. This is a blocking call that will not return +until the algorithm has finished, timed out, or been killed. +""" +run!(r::Runner) = LibSemigroups.run!(r) + +""" + run_for!(r::Runner, t::TimePeriod) + +Run the algorithm for at most the duration `t`. The algorithm may finish +before the time limit, in which case [`finished`](@ref) will return `true`. +If the time limit is reached, [`timed_out`](@ref) will return `true`. + +# Examples +```julia +run_for!(r, Second(1)) +run_for!(r, Millisecond(500)) +``` +""" +function run_for!(r::Runner, t::TimePeriod) + ns = convert(Nanosecond, t) + Dates.value(ns) >= 0 || + throw(ArgumentError("run_for! requires a non-negative duration, got $t")) + LibSemigroups.run_for!(r, Int64(Dates.value(ns))) +end + + +""" + run_until!(f::Function, r::Runner) + run_until!(r::Runner, f::Function) + +Run the algorithm until the nullary predicate `f` returns `true` or the +algorithm [`finished`](@ref). Supports do-block syntax: + +```julia +run_until!(r) do + some_condition(r) +end +``` +""" +function run_until!(f::Function, r::Runner) + sf = @safe_cfunction($f, Cuchar, ()) + GC.@preserve sf LibSemigroups.run_until!(r, sf) +end +run_until!(r::Runner, f::Function) = run_until!(f, r) + +""" + init!(r::Runner) -> Runner + +Re-initialize the runner to its default-constructed state, discarding all +previously computed results. +""" +init!(r::Runner) = LibSemigroups.init!(r) + +# ============================================================================ +# State queries +# ============================================================================ + +""" + finished(r::Runner) -> Bool + +Return `true` if the algorithm has run to completion. +""" +finished(r::Runner) = LibSemigroups.finished(r) + +""" + Base.success(r::Runner) -> Bool + +Return `true` if the algorithm has completed successfully. This extends +`Base.success` (which checks process exit status) to work with libsemigroups +[`Runner`](@ref) types. By default, this returns the same value as +[`finished`](@ref), but derived classes may override this to distinguish +between completion and successful completion. +""" +Base.success(r::Runner) = LibSemigroups.success(r) + +""" + started(r::Runner) -> Bool + +Return `true` if [`run!`](@ref) has been called at least once. +""" +started(r::Runner) = LibSemigroups.started(r) + +""" + running(r::Runner) -> Bool + +Return `true` if the algorithm is currently executing. +""" +running(r::Runner) = LibSemigroups.running(r) + +""" + timed_out(r::Runner) -> Bool + +Return `true` if the last call to [`run_for!`](@ref) exhausted its time limit +without the algorithm finishing. +""" +timed_out(r::Runner) = LibSemigroups.timed_out(r) + +""" + stopped(r::Runner) -> Bool + +Return `true` if the algorithm is stopped for any reason (finished, timed out, +dead, or stopped by predicate). +""" +stopped(r::Runner) = LibSemigroups.stopped(r) + +""" + dead(r::Runner) -> Bool + +Return `true` if the runner was killed (e.g. from another thread via +[`kill!`](@ref)). +""" +dead(r::Runner) = LibSemigroups.dead(r) + +""" + stopped_by_predicate(r::Runner) -> Bool + +Return `true` if the last `run_until` call was stopped because the predicate +was satisfied. +""" +stopped_by_predicate(r::Runner) = LibSemigroups.stopped_by_predicate(r) + +""" + running_for(r::Runner) -> Bool + +Return `true` if the runner is currently executing a [`run_for!`](@ref) call. +""" +running_for(r::Runner) = LibSemigroups.running_for(r) + +""" + running_for_how_long(r::Runner) -> Nanosecond + +Return the duration of the most recent [`run_for!`](@ref) call as a +`Dates.Nanosecond` period. +""" +running_for_how_long(r::Runner) = Nanosecond(LibSemigroups.running_for_how_long(r)) + +""" + running_until(r::Runner) -> Bool + +Return `true` if the runner is currently executing a `run_until` call. +""" +running_until(r::Runner) = LibSemigroups.running_until(r) + +""" + current_state(r::Runner) -> RunnerState + +Return the current [`RunnerState`](@ref) of the runner. +""" +current_state(r::Runner) = LibSemigroups.current_state(r) + +# ============================================================================ +# Control +# ============================================================================ + +""" + kill!(r::Runner) + +Kill the runner. This is thread-safe and can be called from another thread +to stop a running algorithm. After calling `kill!`, [`dead`](@ref) will return +`true`. +""" +kill!(r::Runner) = LibSemigroups.kill!(r) + +# ============================================================================ +# Reporting +# ============================================================================ + +""" + report_why_we_stopped(r::Runner) + +Print the reason the algorithm stopped to `stderr`. +""" +report_why_we_stopped(r::Runner) = LibSemigroups.report_why_we_stopped(r) + +""" + string_why_we_stopped(r::Runner) -> String + +Return a human-readable string describing why the algorithm stopped. +""" +string_why_we_stopped(r::Runner) = LibSemigroups.string_why_we_stopped(r) diff --git a/src/libsemigroups/word-graph.jl b/src/libsemigroups/word-graph.jl new file mode 100644 index 0000000..f242567 --- /dev/null +++ b/src/libsemigroups/word-graph.jl @@ -0,0 +1,125 @@ +# 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. + +""" +word-graph.jl - Julia wrappers for libsemigroups WordGraph + +This file provides low-level Julia wrappers for the C++ WordGraph +class exposed via CxxWrap. + +Indices at this layer are **0-based** (matching C++). The high-level +FroidurePin API will add 1-based conversion when exposing Cayley +graphs to users. +""" + +# ============================================================================ +# Type alias +# ============================================================================ + +""" + WordGraph + +A word graph (deterministic automaton without initial or accept states). +Nodes are numbered `0` to `number_of_nodes(g) - 1` and every node has the +same out-degree. + +Construct with `WordGraph(m, n)` for `m` nodes and out-degree `n`. +""" +const WordGraph = LibSemigroups.WordGraph + +# ============================================================================ +# Size / structure queries +# ============================================================================ + +""" + number_of_nodes(g::WordGraph) -> Int + +Return the number of nodes in the word graph. +""" +number_of_nodes(g::WordGraph) = Int(LibSemigroups.number_of_nodes(g)) + +""" + out_degree(g::WordGraph) -> Int + +Return the out-degree (number of edge labels) of every node. +""" +out_degree(g::WordGraph) = Int(LibSemigroups.out_degree(g)) + +""" + number_of_edges(g::WordGraph) -> Int + +Return the total number of defined edges in the word graph. +""" +number_of_edges(g::WordGraph) = Int(LibSemigroups.number_of_edges(g)) + +""" + number_of_edges(g::WordGraph, s::Integer) -> Int + +Return the number of defined edges with source node `s` (0-based). +""" +number_of_edges(g::WordGraph, s::Integer) = + Int(LibSemigroups.number_of_edges_node(g, UInt32(s))) + +# ============================================================================ +# Edge lookup +# ============================================================================ + +""" + target(g::WordGraph, source::Integer, label::Integer) -> UInt32 + +Return the target of the edge from `source` with `label` (both 0-based). +Returns the C++ `UNDEFINED` value (as `UInt32`) if no such edge exists. +""" +target(g::WordGraph, source::Integer, label::Integer) = + LibSemigroups.target(g, UInt32(source), UInt32(label)) + +""" + next_label_and_target(g::WordGraph, s::Integer, a::Integer) + +Return the next defined edge from node `s` with label >= `a` (both 0-based). +Returns a `(label, target)` tuple; both are `UNDEFINED` if none found. +""" +function next_label_and_target(g::WordGraph, s::Integer, a::Integer) + s32, a32 = UInt32(s), UInt32(a) + return (LibSemigroups.next_label(g, s32, a32), LibSemigroups.next_target(g, s32, a32)) +end + +# ============================================================================ +# Iteration helpers +# ============================================================================ + +""" + targets(g::WordGraph, source::Integer) -> Vector{UInt32} + +Return all edge targets from `source` (0-based) as a vector. Includes +`UNDEFINED` entries for labels with no defined edge. +""" +targets(g::WordGraph, source::Integer) = LibSemigroups.targets_vector(g, UInt32(source)) + +# ============================================================================ +# Comparison operators +# ============================================================================ + +Base.:(==)(a::WordGraph, b::WordGraph) = LibSemigroups.is_equal(a, b) +Base.:(<)(a::WordGraph, b::WordGraph) = LibSemigroups.is_less(a, b) + +# ============================================================================ +# Copy and hash +# ============================================================================ + +Base.copy(g::WordGraph) = LibSemigroups.copy(g) +Base.hash(g::WordGraph, h::UInt) = hash(LibSemigroups.hash(g), h) + +# ============================================================================ +# Display +# ============================================================================ + +function Base.show(io::IO, g::WordGraph) + n = number_of_nodes(g) + e = number_of_edges(g) + d = out_degree(g) + print(io, "WordGraph($n, $d) with $e edges") +end diff --git a/test/runtests.jl b/test/runtests.jl index 7bf6e01..21d3b4c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -14,5 +14,8 @@ using Semigroups @testset "Semigroups.jl" begin include("test_constants.jl") include("test_errors.jl") + include("test_runner.jl") + include("test_word_graph.jl") + include("test_froidure_pin.jl") include("test_transf.jl") end diff --git a/test/test_froidure_pin.jl b/test/test_froidure_pin.jl new file mode 100644 index 0000000..74e9efa --- /dev/null +++ b/test/test_froidure_pin.jl @@ -0,0 +1,187 @@ +# 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.jl - Tests for FroidurePin C++ bindings +""" + +using CxxWrap.StdLib: StdVector + +@testset "FroidurePin type registration" begin + LS = Semigroups.LibSemigroups + + # All 9 types exist + @test isdefined(LS, :FroidurePinTransf1) + @test isdefined(LS, :FroidurePinTransf2) + @test isdefined(LS, :FroidurePinTransf4) + @test isdefined(LS, :FroidurePinPPerm1) + @test isdefined(LS, :FroidurePinPPerm2) + @test isdefined(LS, :FroidurePinPPerm4) + @test isdefined(LS, :FroidurePinPerm1) + @test isdefined(LS, :FroidurePinPerm2) + @test isdefined(LS, :FroidurePinPerm4) + + # All inherit from FroidurePinBase + @test LS.FroidurePinTransf1 <: FroidurePinBase + @test LS.FroidurePinTransf2 <: FroidurePinBase + @test LS.FroidurePinTransf4 <: FroidurePinBase + @test LS.FroidurePinPPerm1 <: FroidurePinBase + @test LS.FroidurePinPPerm2 <: FroidurePinBase + @test LS.FroidurePinPPerm4 <: FroidurePinBase + @test LS.FroidurePinPerm1 <: FroidurePinBase + @test LS.FroidurePinPerm2 <: FroidurePinBase + @test LS.FroidurePinPerm4 <: FroidurePinBase + + # Transitive inheritance from Runner + @test LS.FroidurePinTransf1 <: Runner + @test LS.FroidurePinPPerm1 <: Runner + @test LS.FroidurePinPerm1 <: Runner +end + +@testset "FroidurePin construction and enumeration (Transf)" begin + LS = Semigroups.LibSemigroups + + # S3 = symmetric group on 3 letters (0-based images) + # swap(0,1): [1, 0, 2] + # cycle(0->1->2): [1, 2, 0] + t1 = LS.Transf1(StdVector{UInt8}(UInt8[1, 0, 2])) + t2 = LS.Transf1(StdVector{UInt8}(UInt8[1, 2, 0])) + + fp = LS.FroidurePinTransf1(t1, t2) + + @test !LS.finished(fp) + LS.run!(fp) + @test LS.finished(fp) + @test LS.size(fp) == 6 + @test LS.number_of_generators(fp) == 2 + @test LS.degree(fp) == 3 +end + +@testset "FroidurePin element access" begin + LS = Semigroups.LibSemigroups + + t1 = LS.Transf1(StdVector{UInt8}(UInt8[1, 0, 2])) + t2 = LS.Transf1(StdVector{UInt8}(UInt8[1, 2, 0])) + fp = LS.FroidurePinTransf1(t1, t2) + LS.run!(fp) + + # generator access (0-based) + g0 = LS.generator(fp, UInt32(0)) + @test LS.is_equal(g0, t1) + g1 = LS.generator(fp, UInt32(1)) + @test LS.is_equal(g1, t2) + + # at access (0-based) + e0 = LS.at(fp, UInt32(0)) + @test LS.is_equal(e0, t1) + + # position_element round-trip + pos = LS.position_element(fp, t1) + @test pos == UInt32(0) + + pos2 = LS.position_element(fp, t2) + @test pos2 == UInt32(1) + + # contains_element + @test LS.contains_element(fp, t1) + @test LS.contains_element(fp, t2) +end + +@testset "FroidurePin idempotents" begin + LS = Semigroups.LibSemigroups + + t1 = LS.Transf1(StdVector{UInt8}(UInt8[1, 0, 2])) + t2 = LS.Transf1(StdVector{UInt8}(UInt8[1, 2, 0])) + fp = LS.FroidurePinTransf1(t1, t2) + + # S3 has exactly 1 idempotent (the identity) + @test LS.number_of_idempotents(fp) == 1 +end + +@testset "FroidurePin fast_product" begin + LS = Semigroups.LibSemigroups + + t1 = LS.Transf1(StdVector{UInt8}(UInt8[1, 0, 2])) + t2 = LS.Transf1(StdVector{UInt8}(UInt8[1, 2, 0])) + fp = LS.FroidurePinTransf1(t1, t2) + LS.run!(fp) + + # fast_product(i, j) returns position of fp[i] * fp[j] + prod_pos = LS.fast_product(fp, UInt32(0), UInt32(1)) + @test prod_pos isa UInt32 + @test prod_pos < UInt32(LS.size(fp)) +end + +@testset "FroidurePin inherited FroidurePinBase methods" begin + LS = Semigroups.LibSemigroups + + t1 = LS.Transf1(StdVector{UInt8}(UInt8[1, 0, 2])) + t2 = LS.Transf1(StdVector{UInt8}(UInt8[1, 2, 0])) + fp = LS.FroidurePinTransf1(t1, t2) + LS.run!(fp) + + # number_of_rules works via inheritance + @test LS.number_of_rules(fp) > 0 + + # Cayley graphs work via inheritance + rg = LS.right_cayley_graph(fp) + @test Semigroups.number_of_nodes(rg) == LS.size(fp) + + lg = LS.left_cayley_graph(fp) + @test Semigroups.number_of_nodes(lg) == LS.size(fp) + + # contains_one works via inheritance + @test LS.contains_one(fp) +end + +@testset "FroidurePin with Perm" begin + LS = Semigroups.LibSemigroups + + # S3 with Perm1 + p1 = LS.Perm1(StdVector{UInt8}(UInt8[1, 0, 2])) + p2 = LS.Perm1(StdVector{UInt8}(UInt8[1, 2, 0])) + fp = LS.FroidurePinPerm1(p1, p2) + + @test LS.size(fp) == 6 + @test LS.number_of_generators(fp) == 2 + @test LS.contains_one(fp) +end + +@testset "FroidurePin with PPerm" begin + LS = Semigroups.LibSemigroups + + # S3 as partial perms (total, so same as perms) + pp1 = LS.PPerm1(StdVector{UInt8}(UInt8[1, 0, 2])) + pp2 = LS.PPerm1(StdVector{UInt8}(UInt8[1, 2, 0])) + fp = LS.FroidurePinPPerm1(pp1, pp2) + + @test LS.size(fp) == 6 + @test LS.number_of_generators(fp) == 2 +end + +@testset "FroidurePin elements_vector" begin + LS = Semigroups.LibSemigroups + + t1 = LS.Transf1(StdVector{UInt8}(UInt8[1, 0, 2])) + t2 = LS.Transf1(StdVector{UInt8}(UInt8[1, 2, 0])) + fp = LS.FroidurePinTransf1(t1, t2) + + elems = LS.elements_vector(fp) + @test length(elems) == 6 +end + +@testset "FroidurePin copy" begin + LS = Semigroups.LibSemigroups + + t1 = LS.Transf1(StdVector{UInt8}(UInt8[1, 0, 2])) + t2 = LS.Transf1(StdVector{UInt8}(UInt8[1, 2, 0])) + fp = LS.FroidurePinTransf1(t1, t2) + LS.run!(fp) + + fp2 = LS.copy(fp) + @test LS.size(fp2) == 6 + @test LS.finished(fp2) +end diff --git a/test/test_runner.jl b/test/test_runner.jl new file mode 100644 index 0000000..4eaedb2 --- /dev/null +++ b/test/test_runner.jl @@ -0,0 +1,73 @@ +# 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_runner.jl - Tests for Runner base class bindings + +Runner is abstract, so we cannot instantiate it directly. These tests verify +that types, constants, and method definitions are correctly bound. Behavioral +tests are exercised via concrete derived classes (e.g. FroidurePin) once they +are available. +""" + +using Dates: TimePeriod + +@testset "Runner type aliases" begin + @test Runner === Semigroups.LibSemigroups.Runner + @test RunnerState === Semigroups.LibSemigroups.state +end + +@testset "RunnerState constants" begin + # All state constants are accessible and have the right type + states = [ + STATE_NEVER_RUN, + STATE_RUNNING_TO_FINISH, + STATE_RUNNING_FOR, + STATE_RUNNING_UNTIL, + STATE_TIMED_OUT, + STATE_STOPPED_BY_PREDICATE, + STATE_NOT_RUNNING, + STATE_DEAD, + ] + + for s in states + @test s isa RunnerState + end + + # All state constants are distinct + for i in eachindex(states), j in eachindex(states) + if i != j + @test states[i] != states[j] + end + end +end + +@testset "Runner method definitions" begin + # Verify that each wrapper function is defined with the correct + # signature dispatching on Runner + @test hasmethod(run!, Tuple{Runner}) + @test hasmethod(run_for!, Tuple{Runner,TimePeriod}) + @test hasmethod(run_until!, Tuple{Function,Runner}) + @test hasmethod(run_until!, Tuple{Runner,Function}) + @test hasmethod(init!, Tuple{Runner}) + @test hasmethod(kill!, Tuple{Runner}) + + @test hasmethod(finished, Tuple{Runner}) + @test hasmethod(success, Tuple{Runner}) + @test hasmethod(started, Tuple{Runner}) + @test hasmethod(running, Tuple{Runner}) + @test hasmethod(timed_out, Tuple{Runner}) + @test hasmethod(stopped, Tuple{Runner}) + @test hasmethod(dead, Tuple{Runner}) + @test hasmethod(stopped_by_predicate, Tuple{Runner}) + @test hasmethod(running_for, Tuple{Runner}) + @test hasmethod(running_for_how_long, Tuple{Runner}) + @test hasmethod(running_until, Tuple{Runner}) + @test hasmethod(current_state, Tuple{Runner}) + + @test hasmethod(report_why_we_stopped, Tuple{Runner}) + @test hasmethod(string_why_we_stopped, Tuple{Runner}) +end diff --git a/test/test_word_graph.jl b/test/test_word_graph.jl new file mode 100644 index 0000000..88cf7a1 --- /dev/null +++ b/test/test_word_graph.jl @@ -0,0 +1,90 @@ +# 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_word_graph.jl - Tests for WordGraph bindings +""" + +@testset "WordGraph type alias" begin + @test WordGraph === Semigroups.LibSemigroups.WordGraph +end + +@testset "WordGraph construction" begin + g = WordGraph(5, 3) + @test number_of_nodes(g) == 5 + @test out_degree(g) == 3 + @test number_of_edges(g) == 0 # no edges defined yet + + # Default: zero nodes, zero out-degree + g0 = WordGraph(0, 0) + @test number_of_nodes(g0) == 0 + @test out_degree(g0) == 0 + @test number_of_edges(g0) == 0 +end + +@testset "WordGraph number_of_edges per node" begin + g = WordGraph(3, 2) + # No edges defined, so each node has 0 defined edges + for i = 0:2 + @test number_of_edges(g, i) == 0 + end +end + +@testset "WordGraph target on empty graph" begin + g = WordGraph(3, 2) + # All targets should be UNDEFINED + for node = 0:2, label = 0:1 + t = Semigroups.target(g, node, label) + @test is_undefined(t, UInt32) + end +end + +@testset "WordGraph targets vector" begin + g = WordGraph(3, 2) + # All targets from node 0 should be UNDEFINED + ts = Semigroups.targets(g, 0) + @test length(ts) == 2 # out_degree is 2 + for t in ts + @test is_undefined(t, UInt32) + end +end + +@testset "WordGraph comparison" begin + g1 = WordGraph(3, 2) + g2 = WordGraph(3, 2) + @test g1 == g2 + + g3 = WordGraph(4, 2) + @test g1 != g3 +end + +@testset "WordGraph copy" begin + g1 = WordGraph(3, 2) + g2 = copy(g1) + @test g1 == g2 +end + +@testset "WordGraph hash" begin + g1 = WordGraph(3, 2) + g2 = WordGraph(3, 2) + @test hash(g1) == hash(g2) + + # Can be used in Sets + s = Set([g1]) + @test g2 in s +end + +@testset "WordGraph method signatures" begin + @test hasmethod(number_of_nodes, Tuple{WordGraph}) + @test hasmethod(out_degree, Tuple{WordGraph}) + @test hasmethod(number_of_edges, Tuple{WordGraph}) + @test hasmethod(number_of_edges, Tuple{WordGraph,Integer}) + @test hasmethod(Semigroups.target, Tuple{WordGraph,Integer,Integer}) + @test hasmethod(Semigroups.next_label_and_target, Tuple{WordGraph,Integer,Integer}) + @test hasmethod(Semigroups.targets, Tuple{WordGraph,Integer}) + @test hasmethod(copy, Tuple{WordGraph}) + @test hasmethod(hash, Tuple{WordGraph,UInt}) +end