diff --git a/deps/src/CMakeLists.txt b/deps/src/CMakeLists.txt index 4f33dda..791fcb6 100644 --- a/deps/src/CMakeLists.txt +++ b/deps/src/CMakeLists.txt @@ -60,6 +60,7 @@ add_library(libsemigroups_julia SHARED presentation-examples.cpp knuth-bendix.cpp todd-coxeter.cpp + kambites.cpp ) # Include directories diff --git a/deps/src/kambites.cpp b/deps/src/kambites.cpp new file mode 100644 index 0000000..289e78b --- /dev/null +++ b/deps/src/kambites.cpp @@ -0,0 +1,183 @@ +// +// Semigroups.jl +// Copyright (C) 2026, James W. Swent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +// CRITICAL: libsemigroups_julia.hpp MUST be included first (fmt consteval fix) +#include "libsemigroups_julia.hpp" + +#include + +// kambites-class.hpp and kambites-helpers.hpp MUST come BEFORE cong-common.hpp +// so the template bodies in cong-common.hpp see Kambites-specific overloads of +// congruence_common helpers. ADL resolves these at template-instantiation time; +// see the include-order requirement documented at cong-common.hpp:27-38. +#include +#include + +// Required for `congruence_common::normal_forms(Kambites&)` (used by the +// `kambites_normal_forms_take` binding below). The returned +// `KambitesNormalFormRange::init` calls `libsemigroups::to(k)`, +// whose template definition lives in to-froidure-pin.tpp (transitively +// included by to-froidure-pin.hpp). Without this header, the binding compiles +// but fails to link with an undefined `libsemigroups::to` +// symbol. +#include + +#include "cong-common.hpp" + +#include +#include +#include +#include +#include + +namespace jlcxx { + template <> + struct IsMirroredType> + : std::false_type {}; + + template <> + struct SuperType> { + using type = libsemigroups::detail::CongruenceCommon; + }; +} // namespace jlcxx + +namespace libsemigroups_julia { + + void define_kambites(jl::Module& m) { + using libsemigroups::congruence_kind; + using libsemigroups::Presentation; + using libsemigroups::word_type; + + using CongruenceCommon = libsemigroups::detail::CongruenceCommon; + using K = libsemigroups::Kambites; + + // Type registration + auto type = m.add_type("KambitesWord", + jlcxx::julia_base_type()); + + // Constructors. Direct registration (no defensive lambda); CxxWrap + // converts C++ exceptions through the std::function path for direct + // constructor bindings. + type.constructor<>(); + type.constructor const&>(); + type.constructor(); // copy ctor + + // init! overloads (mirror constructors) + type.method("init!", [](K& self) -> K& { return self.init(); }); + type.method("init!", + [](K& self, + congruence_kind knd, + Presentation const& p) -> K& { + return self.init(knd, p); + }); + + // presentation - return by copy (storage may relocate) + type.method("presentation", [](K const& self) -> Presentation { + return self.presentation(); + }); + + // generating_pairs - return by copy + type.method("generating_pairs", + [](K const& self) -> std::vector { + auto const& pairs = self.generating_pairs(); + return std::vector(pairs.begin(), pairs.end()); + }); + + // kind / number_of_generating_pairs (inherited; exposed for parity with TC) + type.method("kind", + [](K const& self) -> congruence_kind { return self.kind(); }); + + type.method("number_of_generating_pairs", [](K const& self) -> size_t { + return self.number_of_generating_pairs(); + }); + + // number_of_classes + type.method("number_of_classes", + [](K& self) -> uint64_t { return self.number_of_classes(); }); + + // Const-overload split (kambites-class.hpp:731-744): the two + // small_overlap_class() overloads differ only on receiver const-ness, which + // CxxWrap cannot dispatch. Split into two distinctly-named Julia methods. + // + // small_overlap_class -- mutable variant (calls run, returns the class). + type.method("small_overlap_class", + [](K& self) -> size_t { return self.small_overlap_class(); }); + + // current_small_overlap_class -- const variant (returns UNDEFINED if + // unknown). Receiver-by-const-ref selects the const overload. + type.method("current_small_overlap_class", [](K const& self) -> size_t { + return self.small_overlap_class(); + }); + + // throw_if_not_C4 -- bind only the mutable overload, const deferred + type.method("throw_if_not_C4", [](K& self) { self.throw_if_not_C4(); }); + + // TODO: ukkonen() is intentionally NOT bound: its return type is the + // Ukkonen suffix-tree class, which is currently not bound + + // throw_if_letter_not_in_alphabet -- accept ArrayRef, build a + // word_type inside the lambda (mirrors todd-coxeter.cpp:256-260). + type.method("throw_if_letter_not_in_alphabet", + [](K const& self, jlcxx::ArrayRef w) { + word_type ww(w.begin(), w.end()); + self.throw_if_letter_not_in_alphabet(ww.begin(), ww.end()); + }); + + // Display + type.method("to_human_readable_repr", [](K const& self) -> std::string { + return libsemigroups::to_human_readable_repr(self); + }); + + // Cong common helper subset - DO NOT call aggregator + define_cong_common_word_helpers(m); + + // Defense: register a throwing `cong_common_normal_forms` for + // Kambites so the abstract-supertype dispatch path in + // `src/cong-common.jl::normal_forms(::CongruenceCommon)` raises a clear + // LibsemigroupsError if it is ever reached on a Kambites value (rather + // than CxxWrap's opaque method-not-found error). The user-facing + // `normal_forms(::Kambites)` override in `src/kambites.jl` already + // throws ArgumentError for direct calls; this guards the indirect path. + m.method("cong_common_normal_forms", [](K&) -> std::vector { + throw libsemigroups::LibsemigroupsException( + __FILE__, + __LINE__, + __func__, + "Kambites has infinitely many normal forms; use " + "kambites_normal_forms_take (or normal_forms(k, n) on " + "the Julia side) to materialize a finite prefix."); + }); + + // Bounded normal_forms binding (Kambites-specific). Mirrors the cong-common + // normal_forms template but caps iteration at n elements so callers can + // safely take a finite prefix of the infinite normal-form range. + m.method("kambites_normal_forms_take", + [](K& self, size_t n) -> std::vector { + std::vector result; + result.reserve(n); + auto range + = libsemigroups::congruence_common::normal_forms(self); + for (size_t i = 0; i < n && !range.at_end(); ++i) { + result.push_back(range.get()); + range.next(); + } + return result; + }); + } + +} // namespace libsemigroups_julia diff --git a/deps/src/libsemigroups_julia.cpp b/deps/src/libsemigroups_julia.cpp index 94b2d52..e17d477 100644 --- a/deps/src/libsemigroups_julia.cpp +++ b/deps/src/libsemigroups_julia.cpp @@ -55,6 +55,7 @@ namespace libsemigroups_julia { define_presentation_examples(mod); define_knuth_bendix(mod); define_todd_coxeter(mod); + define_kambites(mod); } } // namespace libsemigroups_julia diff --git a/deps/src/libsemigroups_julia.hpp b/deps/src/libsemigroups_julia.hpp index 85b92e1..522f7ec 100644 --- a/deps/src/libsemigroups_julia.hpp +++ b/deps/src/libsemigroups_julia.hpp @@ -70,6 +70,7 @@ namespace libsemigroups_julia { void define_presentation_examples(jl::Module& mod); void define_knuth_bendix(jl::Module& mod); void define_todd_coxeter(jl::Module& mod); + void define_kambites(jl::Module& mod); } // namespace libsemigroups_julia diff --git a/docs/make.jl b/docs/make.jl index 04fb924..6226c81 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -78,6 +78,10 @@ makedocs(; "The KnuthBendix type" => "main-algorithms/knuth-bendix/knuth-bendix.md", "Helper functions" => "main-algorithms/knuth-bendix/helpers.md", ], + "Kambites" => [ + "Overview" => "main-algorithms/kambites/index.md", + "The Kambites type" => "main-algorithms/kambites/kambites.md", + ], ], ], warnonly = [:missing_docs, :linkcheck, :cross_references], diff --git a/docs/src/data-structures/constants/index.md b/docs/src/data-structures/constants/index.md index 31cdce2..9188591 100644 --- a/docs/src/data-structures/constants/index.md +++ b/docs/src/data-structures/constants/index.md @@ -29,3 +29,11 @@ Semigroups.tril_FALSE Semigroups.tril_unknown Semigroups.tril_to_bool ``` + +## Congruence Kind + +```@docs +Semigroups.congruence_kind +Semigroups.onesided +Semigroups.twosided +``` diff --git a/docs/src/main-algorithms/kambites/index.md b/docs/src/main-algorithms/kambites/index.md new file mode 100644 index 0000000..3d4fefd --- /dev/null +++ b/docs/src/main-algorithms/kambites/index.md @@ -0,0 +1,20 @@ +# Kambites + +This section links to the documentation for the algorithms in +Semigroups.jl for small overlap monoids by Mark Kambites and the authors +of libsemigroups. + +| Page | Description | +| ---- | ----------- | +| [The Kambites type](kambites.md) | The [`Kambites`](@ref Semigroups.Kambites) type: construction, queries, the small-overlap-class accessors, validators, and bounded normal forms. | + +Helper functions for [`Kambites`](@ref Semigroups.Kambites) are +documented on the [Common congruence helpers](../cong-common-helpers.md) +page. There are currently no helper functions specific to `Kambites` +beyond those that apply to every +[`CongruenceCommon`](@ref Semigroups.CongruenceCommon) subtype. + +!!! warning "v1 limitation" + Semigroups.jl v1 binds `Kambites{word_type}` only. String-alphabet + presentations are deferred to a later release. Letter indices are + 1-based `Int` values throughout the Julia API. diff --git a/docs/src/main-algorithms/kambites/kambites.md b/docs/src/main-algorithms/kambites/kambites.md new file mode 100644 index 0000000..77de927 --- /dev/null +++ b/docs/src/main-algorithms/kambites/kambites.md @@ -0,0 +1,154 @@ +# The Kambites type + +This page documents the [`Kambites`](@ref Semigroups.Kambites) type, +which implements Kambites's algorithm for the word problem in small +overlap monoids -- finitely presented monoids whose presentation +satisfies the small overlap condition `C(n)` for some `n >= 4`. + +`Kambites` is a subtype of +[`CongruenceCommon`](@ref Semigroups.CongruenceCommon) (and hence of +[`Runner`](@ref Semigroups.Runner)), so all runner methods +([`run!`](@ref), [`run_for!`](@ref), [`finished`](@ref), etc.) and the +shared word-operation helpers ([`reduce`](@ref Semigroups.reduce(::CongruenceCommon, ::AbstractVector{<:Integer})), +[`contains`](@ref Semigroups.contains(::CongruenceCommon, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})), +[`currently_contains`](@ref Semigroups.currently_contains(::CongruenceCommon, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})), +[`add_generating_pair!`](@ref Semigroups.add_generating_pair!(::CongruenceCommon, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})), +[`partition`](@ref Semigroups.partition(::CongruenceCommon, ::AbstractVector{<:AbstractVector{<:Integer}}))) +are available. + +## Table of contents + +| Section | Description | +| ------- | ----------- | +| [Construction and re-initialization](#Construction-and-re-initialization) | Constructors and `init!`. | +| [Queries](#Queries) | Class count and number of generating pairs. | +| [Presentation and generating pairs](#Presentation-and-generating-pairs) | Access the underlying presentation and extra generating pairs. | +| [Small overlap class](#Small-overlap-class) | Compute or read the cached small overlap class `C(n)` of the underlying presentation. | +| [Validators](#Validators) | Throw on invalid letters or insufficient small overlap class. | +| [Normal forms](#Normal-forms) | Bounded enumeration of normal forms (the unbounded form throws). | +| [Non-trivial classes (always throws)](#Non-trivial-classes-always-throws) | Why `non_trivial_classes(::Kambites, ::Kambites)` is intentionally not provided. | +| [Display and copy](#Display-and-copy) | `show`, `copy`. | + +```@docs +Semigroups.Kambites +``` + +## Construction and re-initialization + +| Function | Description | +| -------- | ----------- | +| `Kambites()` | Construct a default `Kambites`; throws on subsequent use until reinitialized via [`init!`](@ref). | +| `Kambites(kind, p)` | Construct from a [`congruence_kind`](@ref Semigroups.congruence_kind) (must be [`twosided`](@ref Semigroups.twosided)) and a [`Presentation`](@ref Semigroups.Presentation). | +| `Kambites(other)` | Copy an existing `Kambites`. | +| [`init!(k)`](@ref Semigroups.init!(::Kambites)) | Reset to default-constructed state, or reinitialize from a new kind and presentation. | + +```@docs +Semigroups.init!(::Kambites) +``` + +## Queries + +| Function | Description | +| -------- | ----------- | +| [`number_of_classes(k)`](@ref Semigroups.number_of_classes(::Kambites)) | Number of congruence classes; returns [`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY) when `small_overlap_class(k) >= 4`. | +| [`number_of_generating_pairs(k)`](@ref Semigroups.number_of_generating_pairs(::Kambites)) | Number of extra generating pairs. | + +```@docs +Semigroups.number_of_classes(::Kambites) +Semigroups.number_of_generating_pairs(::Kambites) +``` + +## Presentation and generating pairs + +| Function | Description | +| -------- | ----------- | +| [`kind(k)`](@ref Semigroups.kind(::Kambites)) | Congruence kind (always `twosided`). | +| [`presentation(k)`](@ref Semigroups.presentation(::Kambites)) | Copy of the underlying presentation. | +| [`generating_pairs(k)`](@ref Semigroups.generating_pairs(::Kambites)) | Extra generating pairs as 1-based word-tuple pairs. | + +```@docs +Semigroups.kind(::Kambites) +Semigroups.presentation(::Kambites) +Semigroups.generating_pairs(::Kambites) +``` + +## Small overlap class + +The small overlap class of the underlying presentation is the largest +`n` such that the presentation satisfies the condition `C(n)`. Kambites's +algorithm decides the word problem when this class is at least `4`. + +| Function | Description | +| -------- | ----------- | +| [`small_overlap_class(k)`](@ref Semigroups.small_overlap_class(::Kambites)) | Compute the small overlap class (may trigger work). | +| [`current_small_overlap_class(k)`](@ref Semigroups.current_small_overlap_class(::Kambites)) | Return the cached value, or [`UNDEFINED`](@ref Semigroups.UNDEFINED) if not yet computed. | + +```@docs +Semigroups.small_overlap_class(::Kambites) +Semigroups.current_small_overlap_class(::Kambites) +``` + +## Validators + +| Function | Description | +| -------- | ----------- | +| [`throw_if_not_C4(k)`](@ref Semigroups.throw_if_not_C4(::Kambites)) | Throw if the small overlap class is less than `4`. | +| [`throw_if_letter_not_in_alphabet(k, w)`](@ref Semigroups.throw_if_letter_not_in_alphabet(::Kambites, ::AbstractVector{<:Integer})) | Throw if `w` contains any letter that is not in the alphabet of `k`'s presentation. | + +```@docs +Semigroups.throw_if_not_C4(::Kambites) +Semigroups.throw_if_letter_not_in_alphabet(::Kambites, ::AbstractVector{<:Integer}) +``` + +## Normal forms + +For `Kambites`, the set of normal forms is infinite, so only the +bounded form `normal_forms(k, n)` is provided. The no-argument form +[`normal_forms(k)`](@ref Semigroups.normal_forms(::Kambites)) throws an +`ArgumentError` to prevent accidental infinite enumeration. + +| Function | Description | +| -------- | ----------- | +| [`normal_forms(k, n)`](@ref Semigroups.normal_forms(::Kambites, ::Integer)) | Return the first `n` normal forms as 1-based `Vector{Int}` words. | +| [`normal_forms(k)`](@ref Semigroups.normal_forms(::Kambites)) | Always throws `ArgumentError`. | + +```@docs +Semigroups.normal_forms(::Kambites, ::Integer) +Semigroups.normal_forms(::Kambites) +``` + +## Non-trivial classes (always throws) + +```@docs +Semigroups.non_trivial_classes(::Kambites, ::Kambites) +``` + +## Word operations + +These functions are defined on +[`CongruenceCommon`](@ref Semigroups.CongruenceCommon) and work on all +congruence types, including `Kambites`. Words are given and returned +as 1-based `Vector{Int}` letter indices. See the +[Common congruence helpers](../cong-common-helpers.md) page for the +full API. + +!!! note + `Semigroups.reduce` and `Semigroups.contains` are not exported to + avoid shadowing `Base.reduce` and `Base.contains`. Use the + module-qualified form: `Semigroups.reduce(k, w)`, + `Semigroups.contains(k, u, v)`. + +| Function | Description | +| -------- | ----------- | +| `Semigroups.reduce(k, w)` | Reduce a word to normal form (triggers a full run). | +| `Semigroups.contains(k, u, v)` | Test if two words are congruent (triggers a full run). | +| `currently_contains(k, u, v)` | Test containment using current state; returns [`tril`](@ref Semigroups.tril). | +| `add_generating_pair!(k, u, v)` | Add an extra generating pair. | +| `partition(k, ws)` | Partition a list of words into congruence classes. | + +## Display and copy + +```@docs +Base.show(::IO, ::Kambites) +Base.copy(::Kambites) +``` diff --git a/src/Semigroups.jl b/src/Semigroups.jl index 9d143e7..1d79ada 100644 --- a/src/Semigroups.jl +++ b/src/Semigroups.jl @@ -83,6 +83,7 @@ include("presentation-examples.jl") include("cong-common.jl") include("knuth-bendix.jl") include("todd-coxeter.jl") +include("kambites.jl") # High-level element types include("bmat8.jl") @@ -375,6 +376,10 @@ export standardize!, is_standardized, current_word_graph, word_graph export current_index_of, word_of, current_word_of export is_non_trivial, tc_redundant_rule +# Kambites +export Kambites +export small_overlap_class, current_small_overlap_class, throw_if_not_C4 + # Transformation types and functions export Transf, PPerm, Perm export degree, rank, image, domain, inverse diff --git a/src/kambites.jl b/src/kambites.jl new file mode 100644 index 0000000..1d5e794 --- /dev/null +++ b/src/kambites.jl @@ -0,0 +1,449 @@ +# Copyright (c) 2026, James W. Swent +# +# Distributed under the terms of the GPL license version 3. + +""" +kambites.jl - Kambites wrapper (Layer 2 + 3) +""" + +# ============================================================================ +# Type alias +# ============================================================================ + +""" + Kambites + +Type implementing small overlap class, equality, and normal forms for +small overlap monoids -- finitely presented monoids whose presentation +satisfies the small overlap condition `C(n)` for `n >= 4` (Kambites, +2009). + +A [`Kambites`](@ref Semigroups.Kambites) instance represents a +congruence on the free monoid or semigroup containing the rules of the +[`Presentation`](@ref Semigroups.Presentation) used to construct the +instance, together with the +[`generating_pairs`](@ref Semigroups.generating_pairs) added via +[`add_generating_pair!`](@ref Semigroups.add_generating_pair!). As such, +generating pairs and presentation rules are interchangeable in the +context of `Kambites` objects. + +A `Kambites` object is constructed from a +[`congruence_kind`](@ref Semigroups.congruence_kind) (which must be +[`twosided`](@ref Semigroups.twosided)) and a +[`Presentation`](@ref Semigroups.Presentation), or copied from another +`Kambites`. + +`Kambites` is a subtype of +[`CongruenceCommon`](@ref Semigroups.CongruenceCommon) (and hence of +[`Runner`](@ref Semigroups.Runner)), so all runner methods (`run!`, +`run_for!`, `finished`, `timed_out`, etc.) and most common congruence +helpers ([`reduce`](@ref Semigroups.reduce), +[`contains`](@ref Semigroups.contains), +[`currently_contains`](@ref Semigroups.currently_contains), +[`add_generating_pair!`](@ref Semigroups.add_generating_pair!), +[`partition`](@ref Semigroups.partition)) work on `Kambites` objects. + +# Structural deviations from `KnuthBendix` / `ToddCoxeter` precedent + +- `Base.length` is intentionally **not** defined for `Kambites`. The + number of classes is always + [`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY) (or throws), + so an alias would silently misbehave with `for i in 1:length(k)`. + Use [`number_of_classes`](@ref Semigroups.number_of_classes) + explicitly. +- [`non_trivial_classes`](@ref Semigroups.non_trivial_classes)`(k1::Kambites, k2::Kambites)` + throws `ArgumentError` (upstream rationale: both `Kambites` instances + always represent infinite-class congruences, so the construction does + not generalize; cf. `kambites-helpers.hpp:128-133`). +- [`normal_forms`](@ref Semigroups.normal_forms)`(k::Kambites)` (no-arg) + throws `ArgumentError` because the underlying normal-form range is + infinite. Use the bounded form `normal_forms(k, n)` to materialize + the first `n` normal forms. +- The `ukkonen()` accessor (and the `Ukkonen` type) is deferred to a + later release + +# Constructors + + Kambites() -> Kambites + Kambites(kind::congruence_kind, p::Presentation) -> Kambites + Kambites(k::Kambites) -> Kambites + +# Throws + +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if `kind` + is not [`twosided`](@ref Semigroups.twosided). + +!!! warning "v1 limitation" + v1 of Semigroups.jl binds `Kambites{word_type}` only. String-alphabet + presentations are deferred to later versions. +""" +const Kambites = LibSemigroups.KambitesWord + +# ============================================================================ +# Initialization +# ============================================================================ + +""" + Kambites(kind::congruence_kind, p::Presentation) -> Kambites + +Construct a [`Kambites`](@ref Semigroups.Kambites) instance representing +a congruence of kind `kind` over the semigroup or monoid defined by the +presentation `p`. + +`Kambites` instances can only be used to compute two-sided congruences, +so `kind` must always be [`twosided`](@ref Semigroups.twosided). The +parameter is included for uniformity of interface with +[`KnuthBendix`](@ref Semigroups.KnuthBendix), `ToddCoxeter`, and +`Congruence`. + +This Julia wrapper builds a default `Kambites` and then calls +[`init!`](@ref Semigroups.init!) so that exceptions raised by +libsemigroups surface as +[`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) (the direct +CxxWrap-bound constructor would surface them as `Base.ErrorException`). + +# Throws + +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if `p` is + not valid. +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if `kind` + is not [`twosided`](@ref Semigroups.twosided). +""" +function Kambites(kind::congruence_kind, p::Presentation) + k = LibSemigroups.KambitesWord() + init!(k, kind, p) + return k +end + +""" + init!(k::Kambites) -> Kambites + init!(k::Kambites, kind::congruence_kind, p::Presentation) -> Kambites + +Re-initialize `k` so that it is in the state it would have been in +immediately after the corresponding constructor. + +The one-argument form puts `k` back into the same state as a newly +default-constructed [`Kambites`](@ref Semigroups.Kambites). + +The three-argument form puts `k` back into the state it would have been +in if it had just been newly constructed from `kind` and `p`. +`Kambites` instances can only be used to compute two-sided congruences, +so `kind` must always be [`twosided`](@ref Semigroups.twosided); the +parameter is included for uniformity of interface with +[`KnuthBendix`](@ref Semigroups.KnuthBendix), `ToddCoxeter`, and +`Congruence`. + +Returns `k` for chaining. + +# Throws + +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if `p` is + not valid (three-argument form only). +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if `kind` + is not [`twosided`](@ref Semigroups.twosided) (three-argument form + only). +""" +function init!(k::Kambites) + @wrap_libsemigroups_call LibSemigroups.init!(k) + return k +end + +function init!(k::Kambites, kind::congruence_kind, p::Presentation) + @wrap_libsemigroups_call LibSemigroups.init!(k, kind, p) + return k +end + +# ============================================================================ +# Accessors +# ============================================================================ + +""" + presentation(k::Kambites) -> Presentation + +Return a copy of the [`Presentation`](@ref Semigroups.Presentation) used +to construct or initialize `k` (if any). + +If `k` was constructed or initialized using a presentation, that +presentation is returned. The Julia binding returns by value (an +independent copy) rather than by reference; mutating the returned +object does not affect `k`. +""" +presentation(k::Kambites) = LibSemigroups.presentation(k) + +""" + generating_pairs(k::Kambites) -> Vector{Tuple{Vector{Int}, Vector{Int}}} + +Return the generating pairs of `k` as a vector of 1-based word pairs. + +These are the pairs added via +[`add_generating_pair!`](@ref Semigroups.add_generating_pair!). Each pair +`(u, v)` is returned as a 2-tuple of 1-based `Vector{Int}` letter +indices. The length of the returned vector equals +[`number_of_generating_pairs`](@ref Semigroups.number_of_generating_pairs). +""" +function generating_pairs(k::Kambites) + flat = LibSemigroups.generating_pairs(k) + result = Tuple{Vector{Int},Vector{Int}}[] + for i = 1:2:length(flat) + push!(result, (_word_from_cpp(flat[i]), _word_from_cpp(flat[i+1]))) + end + return result +end + +""" + kind(k::Kambites) -> congruence_kind + +Return the kind of congruence (one- or two-sided) represented by `k`; +see [`congruence_kind`](@ref Semigroups.congruence_kind) for details. +For [`Kambites`](@ref Semigroups.Kambites) this is always +[`twosided`](@ref Semigroups.twosided), since the constructor enforces +that constraint. + +Complexity: constant. +""" +kind(k::Kambites) = LibSemigroups.kind(k) + +""" + number_of_generating_pairs(k::Kambites) -> Int + +Return the number of generating pairs added to `k`. Equals the length +of [`generating_pairs`](@ref Semigroups.generating_pairs)`(k)`. + +Complexity: constant. +""" +number_of_generating_pairs(k::Kambites) = Int(LibSemigroups.number_of_generating_pairs(k)) + +""" + number_of_classes(k::Kambites) -> UInt64 + +Compute the number of congruence classes of `k`. + +`Kambites` instances can only compute the number of classes when +[`small_overlap_class`](@ref Semigroups.small_overlap_class) is at +least 4, and in that case the number is always +[`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY); otherwise an +exception is thrown. Use +[`is_positive_infinity`](@ref Semigroups.is_positive_infinity) (or +direct comparison `result == POSITIVE_INFINITY`) to detect the infinite +case. + +# Throws + +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if it is + not possible to compute the number of classes because the small + overlap class is too small (`small_overlap_class(k) < 4`). +""" +number_of_classes(k::Kambites) = @wrap_libsemigroups_call LibSemigroups.number_of_classes(k) + +# ============================================================================ +# small_overlap_class (const-overload split) +# ============================================================================ + +""" + small_overlap_class(k::Kambites) -> UInt64 + +Get the small overlap class of the [`Presentation`](@ref +Semigroups.Presentation) underlying `k`. + +If ``S`` is a finitely presented semigroup with generating set ``A``, +then a word ``w`` over ``A`` is a *piece* if ``w`` occurs as a factor +in at least two of the relations defining ``S``, or if it occurs as a +factor of one relation in two different positions (possibly +overlapping). A finitely presented semigroup ``S`` satisfies the +condition ``C(n)`` for a positive integer ``n`` if the minimum number +of pieces in any factorisation of a word occurring as the left or +right hand side of a relation is at least ``n``. + +This function returns the greatest positive integer `n` such that the +finitely presented semigroup represented by `k` satisfies the +condition ``C(n)``, or +[`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY) if no word +occurring in a relation can be written as a product of pieces. It may +trigger computation. + +The return type is `UInt64` (rather than `Int`) because +[`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY) is encoded as +`typemax(UInt64) - 1` on the wire and would not round-trip through +`Int`. Use [`is_positive_infinity`](@ref Semigroups.is_positive_infinity) +(or direct comparison `result == POSITIVE_INFINITY`) to detect the +infinite case. + +Complexity: ``O(m^3)``, where ``m`` is the sum of the lengths of the +words occurring in the relations of the semigroup. + +!!! warning + [`Semigroups.contains`](@ref Semigroups.contains) and + [`Semigroups.reduce`](@ref Semigroups.reduce) only work if the + return value of this function is at least 4. + +# See also + +[`current_small_overlap_class`](@ref Semigroups.current_small_overlap_class) +""" +small_overlap_class(k::Kambites) = LibSemigroups.small_overlap_class(k) + +""" + current_small_overlap_class(k::Kambites) -> Union{UInt64, UndefinedType} + +Get the current value of the small overlap class of `k`, if known. + +Returns the small overlap class if it has already been computed, or +[`UNDEFINED`](@ref Semigroups.UNDEFINED) otherwise. This function does +not trigger any computation. The known value is returned as `UInt64` +(see [`small_overlap_class`](@ref Semigroups.small_overlap_class) for +the rationale). + +See [`small_overlap_class`](@ref Semigroups.small_overlap_class) for +more details on what the small overlap class is. + +# See also + +[`small_overlap_class`](@ref Semigroups.small_overlap_class) +""" +function current_small_overlap_class(k::Kambites) + val = LibSemigroups.current_small_overlap_class(k) + return val == convert(UInt, UNDEFINED) ? UNDEFINED : val +end + +# ============================================================================ +# Validators +# ============================================================================ + +""" + throw_if_letter_not_in_alphabet(k::Kambites, w::AbstractVector{<:Integer}) + +Throw if any letter in `w` is out of bounds. + +This function throws a +[`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if any +letter of `w` is out of bounds -- i.e. does not belong to the alphabet +of the [`Presentation`](@ref Semigroups.Presentation) used to +construct `k`. Letters in `w` are 1-based indices. + +# Arguments + +- `k::Kambites`: the [`Kambites`](@ref Semigroups.Kambites) instance. +- `w::AbstractVector{<:Integer}`: the word to check. + +# Throws + +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if any + letter in `w` is not in the alphabet. +""" +function throw_if_letter_not_in_alphabet(k::Kambites, w::AbstractVector{<:Integer}) + cpp_w = _word_to_cpp(w) + @wrap_libsemigroups_call LibSemigroups.throw_if_letter_not_in_alphabet(k, cpp_w) + return nothing +end + +""" + throw_if_not_C4(k::Kambites) + +Throw if the [`small_overlap_class`](@ref Semigroups.small_overlap_class) +of `k` is not at least 4. + +This function throws an exception if the small overlap class of `k` is +not at least 4 (and computes it if necessary). + +# Throws + +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if + `small_overlap_class(k) < 4`. +""" +function throw_if_not_C4(k::Kambites) + @wrap_libsemigroups_call LibSemigroups.throw_if_not_C4(k) + return nothing +end + +# ============================================================================ +# Base.* overloads +# ============================================================================ + +""" + Base.show(io::IO, k::Kambites) + +Print a human-readable representation of `k`. Delegates to +libsemigroups' `to_human_readable_repr`. +""" +function Base.show(io::IO, k::Kambites) + print(io, LibSemigroups.to_human_readable_repr(k)) +end + +""" + Base.copy(k::Kambites) -> Kambites + +Create an independent copy of `k` via the C++ copy constructor. +""" +Base.copy(k::Kambites) = LibSemigroups.KambitesWord(k) + +Base.deepcopy_internal(k::Kambites, ::IdDict) = LibSemigroups.KambitesWord(k) + +# ============================================================================ +# non_trivial_classes override (throws) +# ============================================================================ + +""" + non_trivial_classes(k1::Kambites, k2::Kambites) + +Always throws `ArgumentError` for [`Kambites`](@ref Semigroups.Kambites) +arguments. + +`non_trivial_classes(Kambites, Kambites)` is intentionally not provided +upstream (see `kambites-helpers.hpp:128-133`) because both `Kambites` +instances always represent infinite-class congruences, so the +construction does not generalize. +""" +function non_trivial_classes(::Kambites, ::Kambites) + throw( + ArgumentError( + "non_trivial_classes(::Kambites, ::Kambites) is intentionally not " * + "supported: both Kambites instances always represent infinite-class " * + "congruences, so the construction does not generalize " * + "(see kambites-helpers.hpp:128-133).", + ), + ) +end + +# ============================================================================ +# normal_forms (bounded; the no-arg form throws) +# ============================================================================ + +""" + normal_forms(k::Kambites, n::Integer) -> Vector{Vector{Int}} + +Return the first `n` short-lex normal forms of the classes of the +congruence represented by `k`, as 1-based `Vector{Int}` words. + +The underlying range of normal forms is always infinite (one per +congruence class, and a `C(>=4)` presentation has infinitely many +classes), so the caller must specify the bound `n`. The bounded form +materializes only the first `n` words and is safe to call. + +# Throws + +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if + `small_overlap_class(k) < 4`. +- `InexactError` if `n` is negative. +""" +function normal_forms(k::Kambites, n::Integer) + cpp_n = UInt(n) + nf = @wrap_libsemigroups_call LibSemigroups.kambites_normal_forms_take(k, cpp_n) + return [_word_from_cpp(w) for w in nf] +end + +""" + normal_forms(k::Kambites) + +Always throws `ArgumentError`. The set of normal forms of a `Kambites` +is infinite, so the no-argument form is unsafe; use the bounded form +[`normal_forms(k, n)`](@ref Semigroups.normal_forms(::Kambites, ::Integer)) +instead. +""" +function normal_forms(k::Kambites) + throw( + ArgumentError( + "Kambites has infinitely many normal forms; use normal_forms(k, n) " * + "to take the first n.", + ), + ) +end diff --git a/test/runtests.jl b/test/runtests.jl index 7d4a1ee..4aadc90 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -27,4 +27,5 @@ using Semigroups include("test_knuth_bendix_1.jl") include("test_knuth_bendix_6.jl") include("test_todd_coxeter.jl") + include("test_kambites.jl") end diff --git a/test/test_kambites.jl b/test/test_kambites.jl new file mode 100644 index 0000000..ddea463 --- /dev/null +++ b/test/test_kambites.jl @@ -0,0 +1,191 @@ +# Copyright (c) 2026, James W. Swent +# +# Distributed under the terms of the GPL license version 3. + +""" +test_kambites.jl - Tests for the Kambites Julia API. +""" + +# TODO: Lots of improvements should be made to the +# comprehensiveness of these tests. This was made to +# validate the development process only. + +using Test +using Semigroups + +@testset "Kambites - construction and small-overlap-class on MT4" begin + # Alphabet "abcdefg" = 1..7. Rules: abcd = aaaeaa; ef = dg. + p = Presentation() + set_alphabet!(p, 7) + add_rule_no_checks!(p, [1, 2, 3, 4], [1, 1, 1, 5, 1, 1]) + add_rule_no_checks!(p, [5, 6], [4, 7]) + + k = Kambites(twosided, p) + + @test kind(k) == twosided + @test small_overlap_class(k) >= 4 + @test number_of_classes(k) == POSITIVE_INFINITY + + @test Semigroups.contains(k, [1, 2, 3, 4], [1, 1, 1, 5, 1, 1]) + @test Semigroups.contains(k, [5, 6], [4, 7]) + @test Semigroups.contains(k, [1, 1, 1, 1, 1, 5, 6], [1, 1, 1, 1, 1, 4, 7]) + @test Semigroups.contains(k, [5, 6, 1, 2, 1, 2, 1], [4, 7, 1, 2, 1, 2, 1]) +end + +@testset "Kambites - small_overlap_class parametric loop" begin + # For i = 4..19, build over alphabet "ab" = [1, 2]: + # lhs(i) = concat over b = 1..i of (1, b copies of 2) + # rhs(i) = concat over b = i+1..2i of (1, b copies of 2) + # The single rule lhs(i) = rhs(i) yields small_overlap_class == i. + for i = 4:19 + lhs = Int[] + for b = 1:i + push!(lhs, 1) + append!(lhs, fill(2, b)) + end + rhs = Int[] + for b = (i+1):(2*i) + push!(rhs, 1) + append!(rhs, fill(2, b)) + end + + p = Presentation() + set_alphabet!(p, 2) + add_rule_no_checks!(p, lhs, rhs) + + k = Kambites(twosided, p) + @test small_overlap_class(k) == i + end +end + +@testset "Kambites - aabc = acba over a 3-letter alphabet" begin + # Alphabet [c=1, a=2, b=3]; rule aabc = acba -> [2,2,3,1] = [2,1,3,2]. + p = Presentation() + set_alphabet!(p, 3) + add_rule_no_checks!(p, [2, 2, 3, 1], [2, 1, 3, 2]) + + k = Kambites(twosided, p) + + @test !Semigroups.contains(k, [2], [3]) + @test Semigroups.contains(k, [2, 2, 3, 1, 2, 3, 1], [2, 2, 3, 1, 1, 3, 2]) + @test number_of_classes(k) == POSITIVE_INFINITY +end + +@testset "Kambites - free semigroup" begin + # Empty rule set -> small_overlap_class is POSITIVE_INFINITY. + p = Presentation() + set_alphabet!(p, 3) + k = Kambites(twosided, p) + @test small_overlap_class(k) == POSITIVE_INFINITY + + p2 = Presentation() + set_alphabet!(p2, 1) + k2 = Kambites(twosided, p2) + @test small_overlap_class(k2) == POSITIVE_INFINITY +end + +@testset "Kambites - negated containment" begin + # Alphabet [a=1, b=2, c=3, d=4, e=5]; rule cadeca = baedba. + p = Presentation() + set_alphabet!(p, 5) + add_rule_no_checks!(p, [3, 1, 4, 5, 3, 1], [2, 1, 5, 4, 2, 1]) + + k = Kambites(twosided, p) + + # cadece (= [3,1,4,5,3,5]) is not congruent to baedce (= [2,1,5,4,3,5]). + @test !Semigroups.contains(k, [3, 1, 4, 5, 3, 5], [2, 1, 5, 4, 3, 5]) +end + +@testset "Kambites - end-to-end smoke (run!, finished, success, contains, reduce)" begin + p = Presentation() + set_alphabet!(p, 7) + add_rule_no_checks!(p, [1, 2, 3, 4], [1, 1, 1, 5, 1, 1]) + add_rule_no_checks!(p, [5, 6], [4, 7]) + + k = Kambites(twosided, p) + run!(k) + @test finished(k) + @test success(k) + + @test Semigroups.contains(k, [1, 2, 3, 4], [1, 1, 1, 5, 1, 1]) + + r = Semigroups.reduce(k, [1, 2, 3, 4]) + @test r isa Vector{Int} + @test Semigroups.contains(k, r, [1, 2, 3, 4]) +end + +@testset "Kambites - bounded normal_forms" begin + p = Presentation() + set_alphabet!(p, 7) + add_rule_no_checks!(p, [1, 2, 3, 4], [1, 1, 1, 5, 1, 1]) + add_rule_no_checks!(p, [5, 6], [4, 7]) + + k = Kambites(twosided, p) + + nfs = normal_forms(k, 20) + @test length(nfs) == 20 + @test all(w -> w isa Vector{Int}, nfs) + + # The no-arg form must throw rather than hang on the infinite range. + @test_throws ArgumentError normal_forms(k) +end + +@testset "Kambites - copy round-trip" begin + p = Presentation() + set_alphabet!(p, 7) + add_rule_no_checks!(p, [1, 2, 3, 4], [1, 1, 1, 5, 1, 1]) + add_rule_no_checks!(p, [5, 6], [4, 7]) + + k = Kambites(twosided, p) + k2 = copy(k) + + @test kind(k2) == twosided + @test small_overlap_class(k2) == small_overlap_class(k) + + run!(k) + @test kind(k2) == twosided +end + +@testset "Kambites - show" begin + p = Presentation() + set_alphabet!(p, 7) + add_rule_no_checks!(p, [5, 6], [4, 7]) + k = Kambites(twosided, p) + + s = sprint(show, k) + @test s isa String + @test !isempty(s) +end + +@testset "Kambites - error paths" begin + p = Presentation() + set_alphabet!(p, 7) + add_rule_no_checks!(p, [1, 2, 3, 4], [1, 1, 1, 5, 1, 1]) + add_rule_no_checks!(p, [5, 6], [4, 7]) + + # Kambites accepts only twosided congruences. + @test_throws LibsemigroupsError Kambites(Semigroups.onesided, p) + + # `non_trivial_classes(::Kambites, ::Kambites)` is intentionally + # unsupported (both arguments always represent infinite-class + # congruences); the override throws ArgumentError. + k1 = Kambites(twosided, p) + k2 = Kambites(twosided, p) + @test_throws ArgumentError non_trivial_classes(k1, k2) + + # throw_if_not_C4 throws when small_overlap_class < 4. The presentation + # `{aa = b}` over a 2-letter alphabet has small_overlap_class < 4. + p_low = Presentation() + set_alphabet!(p_low, 2) + add_rule_no_checks!(p_low, [1, 1], [2]) + k_low = Kambites(twosided, p_low) + @test small_overlap_class(k_low) < 4 + @test_throws LibsemigroupsError throw_if_not_C4(k_low) +end + +@testset "Kambites - design constraint: no Base.length" begin + # `Base.length` is intentionally NOT defined for Kambites because + # `number_of_classes` is always-infinite-or-throws; defining `length` + # as an alias would silently misbehave with `for i in 1:length(k)`. + @test !hasmethod(Base.length, Tuple{Kambites}) +end