diff --git a/docs/pictures/stephen-inverse-word-graph-default.svg b/docs/pictures/stephen-inverse-word-graph-default.svg new file mode 100644 index 00000000..5618cea1 --- /dev/null +++ b/docs/pictures/stephen-inverse-word-graph-default.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/pictures/stephen-inverse-word-graph.svg b/docs/pictures/stephen-inverse-word-graph.svg new file mode 100644 index 00000000..8ece8b6d --- /dev/null +++ b/docs/pictures/stephen-inverse-word-graph.svg @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/data-structures/presentations/present-helpers.rst b/docs/source/data-structures/presentations/present-helpers.rst index 92c7fcac..d6d40e40 100644 --- a/docs/source/data-structures/presentations/present-helpers.rst +++ b/docs/source/data-structures/presentations/present-helpers.rst @@ -53,6 +53,7 @@ Contents greedy_reduce_length greedy_reduce_length_and_number_of_gens index_rule + inverse_alphabet is_normalized is_rule is_strongly_compressible diff --git a/docs/source/data-structures/visualisation/dot.rst b/docs/source/data-structures/visualisation/dot.rst index a3d03cd3..fb30558f 100644 --- a/docs/source/data-structures/visualisation/dot.rst +++ b/docs/source/data-structures/visualisation/dot.rst @@ -34,6 +34,7 @@ Contents Dot.name Dot.node Dot.nodes + Dot.rm_node Dot.subgraphs Dot.to_string diff --git a/src/dot.cpp b/src/dot.cpp index 85565b70..01e61a50 100644 --- a/src/dot.cpp +++ b/src/dot.cpp @@ -370,6 +370,35 @@ the represented graph and ``False`` otherwise. :returns: Whether or not *name* is the name of a node. :rtype: bool )pbdoc"); + + dot.def( + "rm_node", + [](Dot& self, std::string const& name) { self.rm_node(name); }, + py::arg("name"), + R"pbdoc( +:sig=(self: Dot, name: str) -> None: + +Remove a node from the represented graph. + +This function removes the node named *name*. + +:param name: the name of the node to remove. +:type name: str + +:raises LibsemigroupsError: if there is no node named *name*. + +.. doctest:: + + >>> from libsemigroups_pybind11 import Dot + >>> graph = Dot() + >>> node = graph.add_node("a") + >>> graph.is_node("a") + True + >>> graph.rm_node("a") + >>> graph.is_node("a") + False +)pbdoc"); + dot.def("kind", py::overload_cast(&Dot::kind), py::arg("val"), diff --git a/src/libsemigroups_pybind11/presentation/__init__.py b/src/libsemigroups_pybind11/presentation/__init__.py index d0fdf256..fd03b14e 100644 --- a/src/libsemigroups_pybind11/presentation/__init__.py +++ b/src/libsemigroups_pybind11/presentation/__init__.py @@ -32,6 +32,7 @@ presentation_greedy_reduce_length as _greedy_reduce_length, presentation_greedy_reduce_length_and_number_of_gens as _greedy_reduce_length_and_num_of_gens, presentation_index_rule as _index_rule, + presentation_inverse_alphabet as _inverse_alphabet, presentation_is_normalized as _is_normalized, presentation_is_rule as _is_rule, presentation_is_strongly_compressible as _is_strongly_compressible, @@ -249,6 +250,7 @@ def __init__(self: _Self, *args, **kwargs) -> None: first_unused_letter = _wrap_cxx_free_fn(_first_unused_letter) greedy_reduce_length = _wrap_cxx_free_fn(_greedy_reduce_length) greedy_reduce_length_and_number_of_gens = _wrap_cxx_free_fn(_greedy_reduce_length_and_num_of_gens) +inverse_alphabet = _wrap_cxx_free_fn(_inverse_alphabet) is_strongly_compressible = _wrap_cxx_free_fn(_is_strongly_compressible) length = _wrap_cxx_free_fn(_length) longest_rule = _wrap_cxx_free_fn(_longest_rule) @@ -299,6 +301,7 @@ def __init__(self: _Self, *args, **kwargs) -> None: "first_unused_letter", "greedy_reduce_length", "greedy_reduce_length_and_number_of_gens", + "inverse_alphabet", "is_strongly_compressible", "length", "longest_rule", diff --git a/src/present.cpp b/src/present.cpp index 5e5c34fb..094f9f64 100644 --- a/src/present.cpp +++ b/src/present.cpp @@ -1216,25 +1216,68 @@ identity, then this generator is returned. :raises LibsemigroupsError: if :any:`replace_word` or :any:`add_identity_rules` does. +)pbdoc"); + m.def("presentation_inverse_alphabet", + &presentation::inverse_alphabet, + py::arg("p"), + R"pbdoc( +:sig=(p: InversePresentation) -> Word: +:only-document-once: + +Return an inverse semigroup generating set. + +This function returns an inverse semigroup generating set of the semigroup +defined by an inverse presentation. More specifically, for every letter +:math:`x` in the alphabet of *p*, the returned word contains precisely one of +:math:`x` and :math:`x^{-1}`. + +:param p: the inverse presentation. +:type p: InversePresentation + +:returns: The inverse semigroup generating set. +:rtype: :ref:`Word` + +:raises LibsemigroupsError: + if :any:`InversePresentation.throw_if_bad_alphabet_rules_or_inverses` raises + an exception. + +.. doctest:: + + >>> from libsemigroups_pybind11 import InversePresentation, presentation + >>> p = InversePresentation("abcABC").inverses("ABCabc") + >>> presentation.inverse_alphabet(p) + 'abc' )pbdoc"); m.def("presentation_normalize_alphabet", - &presentation::normalize_alphabet, + py::overload_cast( + &presentation::normalize_alphabet), py::arg("p"), R"pbdoc( -:sig=(p: Presentation) -> None: +:sig=(p: Presentation | InversePresentation) -> None: :only-document-once: Normalize the alphabet to :math:`\{0, \ldots, n - 1\}`. Modify the presentation in-place so that the alphabet is :math:`\{0, \ldots, -n - 1\}` (or equivalent) and rewrites the rules to use this alphabet. If the -alphabet is already normalized, then no changes are made to the presentation. +n - 1\}` (or equivalent) and rewrites the rules to use this alphabet. For an +:any:`InversePresentation`, the inverses are also rewritten. If the alphabet is +already normalized, then no changes are made to the presentation. :param p: the presentation. -:type p: Presentation +:type p: Presentation | InversePresentation :raises LibsemigroupsError: - if :any:`Presentation.throw_if_bad_alphabet_or_rules` raises an exception on - the initial presentation.)pbdoc"); + if the relevant presentation validity check raises an exception on the + initial presentation. + +.. doctest:: + + >>> from libsemigroups_pybind11 import InversePresentation, presentation + >>> p = InversePresentation("xXyY").inverses("XxYy") + >>> p.rules = ["xyX", "Yx"] + >>> presentation.normalize_alphabet(p) + >>> p.alphabet(), p.rules, p.inverses() + ('abcd', ['acb', 'da'], 'badc') +)pbdoc"); m.def("presentation_reduce_complements", &presentation::reduce_complements, py::arg("p"), @@ -2102,6 +2145,11 @@ defined in the alphabet, and that the inverses act as semigroup inverses. * :any:`Presentation.throw_if_bad_alphabet_or_rules` * :any:`presentation.throw_if_bad_inverses` )pbdoc"); + m.def("presentation_normalize_alphabet", + py::overload_cast( + &presentation::normalize_alphabet), + py::arg("p"), + py::prepend()); } // bind_inverse_present } // namespace diff --git a/src/stephen.cpp b/src/stephen.cpp index 09d741c0..f964a1cc 100644 --- a/src/stephen.cpp +++ b/src/stephen.cpp @@ -16,6 +16,9 @@ // along with this program. If not, see . // +// C++ stl headers.... +#include // for optional, nullopt + // libsemigroups headers #include // for POSITIVE_INFINITY #include // for Stephen @@ -120,7 +123,7 @@ already), and then returns the accept state of the produced word graph. :py:meth:`~Stephen.set_word`. .. warning:: - Termination of the Stephen algorithm is undecidable in general, and + Termination of Stephen's procedure is undecidable in general, and this function may never terminate. )pbdoc"); // The next one is complicated to use/doc so omitted. @@ -177,7 +180,7 @@ the last presentation change and ``False`` otherwise. }, py::arg("other"), R"pbdoc( -Append a Stephen object. +Append a :any:`Stephen` object. This function appends the :any:`Stephen` object *other* to *self*. This modifies the current :any:`Stephen` instance in-place. The result is a @@ -187,7 +190,7 @@ or *other* has already been (partially) run, then we can reuse the underlying word graphs instead of having to recompute them completely from scratch. -:param other: the Stephen instance to append. +:param other: the :any:`Stephen` instance to append. :type other: Stephen :returns: *self*. @@ -309,7 +312,7 @@ word *w* is accepted if and only if :math:`uu^{-1}w` is equivalent to :math:`u` in the semigroup defined by :any:`Stephen.presentation`, where :math:`u` is the value of :any:`Stephen.word`. -:param s: the Stephen instance. +:param s: the :any:`Stephen` instance. :type s: Stephen :param w: the input word. @@ -323,26 +326,193 @@ word *w* is accepted if and only if :math:`uu^{-1}w` is equivalent to :any:`Stephen.init` or if no word was set with :any:`Stephen.set_word`. .. warning:: - Termination of the Stephen algorithm is undecidable in general, and + Termination of Stephen's procedure is undecidable in general, and this function may never terminate. )pbdoc"); - m.def("stephen_dot", - &stephen::dot, + if constexpr (is_specialization_of_v) { + m.def( + "stephen_dot", + [](Stephen_& s, + std::optional const& alphabet, + int_or_constant radius, + bool use_inverse_literals) { + if (alphabet.has_value()) { + return stephen::dot( + s, *alphabet, to_int(radius), use_inverse_literals); + } + return stephen::dot( + s, to_int(radius), use_inverse_literals); + }, + py::arg("s"), + py::kw_only(), + py::arg("alphabet") = std::nullopt, + py::arg("radius") = POSITIVE_INFINITY, + py::arg("use_inverse_literals") = true, + R"pbdoc( +:sig=(s: Stephen, *, alphabet: str | list[int] | None = None, radius: int | PositiveInfinity = POSITIVE_INFINITY, use_inverse_literals: bool = True) -> Dot: +:only-document-once: + +Returns a :any:`Dot` object representing an inverse :any:`Stephen` word graph. + +This function returns a :any:`Dot` object representing the word graph of the +:any:`Stephen` instance *s* in its current state. The returned graph contains +the nodes reachable from the initial state by a path of length at most +*radius*. If *alphabet* is ``None``, edges are labelled by the inverse +semigroup generating set. Otherwise, only edges labelled by letters in +*alphabet* are included. + +If *use_inverse_literals* is ``True``, a letter outside the selected alphabet +in a node label is rendered as its inverse followed by a superscript ``-1``. +Otherwise, every letter in a node label is rendered directly. + +:param s: the :any:`Stephen` instance. +:type s: Stephen + +:param alphabet: + the letters labelling edges in the returned graph, or ``None`` to use the + inverse semigroup generating set. This is a keyword-only argument. +:type alphabet: str | list[int] | None + +:param radius: + the maximum distance from the initial state of a node in the returned graph + (default: :any:`POSITIVE_INFINITY`). This is a keyword-only argument. +:type radius: int | PositiveInfinity + +:param use_inverse_literals: + whether to use inverse literals in node labels (default: ``True``). This is a + keyword-only argument. +:type use_inverse_literals: bool + +:returns: A :any:`Dot` object representing the word graph of *s*. +:rtype: Dot + +:raises LibsemigroupsError: + if no presentation or word has been set in *s*; if *alphabet* contains + duplicate or invalid letters; if a selected letter cannot be rendered + unambiguously as a single character; or if the selected alphabet has more + letters than the number of colours in :any:`Dot.colors`. + +.. note:: + This function does not run *s*. + +.. doctest:: + + >>> from libsemigroups_pybind11 import InversePresentation, Stephen, stephen + >>> p = InversePresentation("abcABC").inverses("ABCabc") + >>> s = Stephen(p).set_word("aBbcaABAabCc") + >>> s.run() + >>> graph = stephen.dot(s, alphabet="abc", radius=2) + >>> graph.node("3").attrs["label"] + '"ab⁻¹"' + >>> graph = stephen.dot( + ... s, alphabet="abc", radius=2, use_inverse_literals=False + ... ) + >>> graph.node("3").attrs["label"] + '"aB"' + +Calling ``view`` without any options displays the complete current graph: + +.. code-block:: python + + stephen.dot(s).view() + +.. figure:: ../../../pictures/stephen-inverse-word-graph-default.svg + :alt: The complete inverse Stephen word graph displayed by view. + :align: center + :width: 420px + + The output of ``stephen.dot(s).view()`` for the presentation and word above. + +The following example stops Stephen's procedure after the word graph reaches +at least 1024 nodes, and displays the part within distance 3 of the initial +state. + +.. doctest:: + + >>> p = InversePresentation("abcABC").inverses("ABCabc") + >>> p.contains_empty_word(True) + + >>> p.rules = ["acb", "", "aCb", ""] + >>> s = Stephen(p).set_word("") + >>> s.run_until(lambda: s.word_graph().number_of_nodes() >= 1024) + >>> graph = stephen.dot(s, radius=3) + >>> len(graph.nodes()), len(graph.edges()) + (11, 17) + +The final call produces the following graph: + +.. figure:: ../../../pictures/stephen-inverse-word-graph.svg + :alt: An inverse Stephen word graph with inverse literals in its node labels. + :align: center + :width: 340px + + The radius-3 inverse Stephen word graph from the example above. +)pbdoc"); + } else { + m.def( + "stephen_dot", + [](Stephen_& s, + std::optional const& alphabet, + int_or_constant radius) { + if (alphabet.has_value()) { + return stephen::dot(s, *alphabet, to_int(radius)); + } + return stephen::dot(s, to_int(radius)); + }, py::arg("s"), + py::kw_only(), + py::arg("alphabet") = std::nullopt, + py::arg("radius") = POSITIVE_INFINITY, R"pbdoc( -:sig=(s: Stephen) -> Dot: +:sig=(s: Stephen, *, alphabet: str | list[int] | None = None, radius: int | PositiveInfinity = POSITIVE_INFINITY) -> Dot: :only-document-once: -Return a :any:`Dot` object representing the underlying word graph of the -:any:`Stephen` object *s*. +Returns a :any:`Dot` object representing a :any:`Stephen` word graph. + +This function returns a :any:`Dot` object representing the word graph of the +:any:`Stephen` instance *s* in its current state. The returned graph contains +the nodes reachable from the initial state by a path of length at most +*radius*. If *alphabet* is ``None``, edges labelled by every letter in the +alphabet of the presentation are included. Otherwise, only edges labelled by +letters in *alphabet* are included. -:param s: the Stephen object. +:param s: the :any:`Stephen` instance. :type s: Stephen -:returns: A :any:`Dot` object. +:param alphabet: + the letters labelling edges in the returned graph, or ``None`` to use the + presentation's alphabet. This is a keyword-only argument. +:type alphabet: str | list[int] | None + +:param radius: + the maximum distance from the initial state of a node in the returned graph + (default: :any:`POSITIVE_INFINITY`). This is a keyword-only argument. +:type radius: int | PositiveInfinity + +:returns: A :any:`Dot` object representing the word graph of *s*. :rtype: Dot + +:raises LibsemigroupsError: + if no presentation or word has been set in *s*; if *alphabet* contains + duplicate or invalid letters; if a selected letter cannot be rendered + unambiguously as a single character; or if the selected alphabet has more + letters than the number of colours in :any:`Dot.colors`. + +.. note:: + This function does not run *s*. + +.. doctest:: + + >>> from libsemigroups_pybind11 import Presentation, Stephen, stephen + >>> p = Presentation("ab") + >>> s = Stephen(p).set_word("abba") + >>> graph = stephen.dot(s, alphabet="a", radius=1) + >>> len(graph.nodes()), len(graph.edges()) + (4, 2) )pbdoc"); + } m.def("stephen_is_left_factor", &stephen::is_left_factor, @@ -359,7 +529,7 @@ left factor of :any:`Stephen.word` in the semigroup defined by :any:`Stephen.presentation`. A word is a left factor of :any:`Stephen.word` if it labels a path in :any:`Stephen.word_graph` with source ``0``. -:param s: the Stephen instance. +:param s: the :any:`Stephen` instance. :type s: Stephen :param w: the input word. @@ -374,7 +544,7 @@ it labels a path in :any:`Stephen.word_graph` with source ``0``. :any:`Stephen.set_word`. .. warning:: - Termination of the Stephen algorithm is undecidable in general, and + Termination of Stephen's procedure is undecidable in general, and this function may never terminate. )pbdoc"); @@ -394,7 +564,7 @@ that are left factors of :any:`Stephen.word`. This function triggers the algorithm implemented in this class (if it hasn't been triggered already). -:param s: the Stephen instance. +:param s: the :any:`Stephen` instance. :type s: Stephen :returns: A :any:`Paths` object containing all the words (in short-lex order) @@ -407,7 +577,7 @@ been triggered already). was set with :any:`Stephen.set_word`. .. warning:: - Termination of the Stephen algorithm is undecidable in general, and + Termination of Stephen's procedure is undecidable in general, and this function may never terminate. )pbdoc"); @@ -430,7 +600,7 @@ the number of paths in :any:`Stephen.word_graph` (if the inherited :any:`Runner.run` method of *s* has been called) with source ``0`` and length in the range *min* to *max*. -:param s: the Stephen instance. +:param s: the :any:`Stephen` instance. :type s: Stephen :param min: the minimum length of a word (default: 0). @@ -450,7 +620,7 @@ in the range *min* to *max*. was set with :any:`Stephen.set_word`. .. warning:: - Termination of the Stephen algorithm is undecidable in general, and + Termination of Stephen's procedure is undecidable in general, and this function may never terminate. )pbdoc"); @@ -483,7 +653,7 @@ equivalent to :math:`u` in the semigroup defined by :any:`Stephen.presentation`, where :math:`u` is the value of :any:`Stephen.word`. -:param s: the Stephen instance. +:param s: the :any:`Stephen` instance. :type s: Stephen :param min: the minimum length of a word (default: ``0``). @@ -503,7 +673,7 @@ equivalent to :math:`u` in the semigroup defined by :any:`Stephen.set_word`. .. warning:: - Termination of the Stephen algorithm is undecidable in general, and + Termination of Stephen's procedure is undecidable in general, and this function may never terminate. )pbdoc"); @@ -524,7 +694,7 @@ Returns a :any:`Paths` object containing all words accepted by a This function triggers the algorithm implemented in this class (if it hasn't been triggered already). -:param s: the Stephen instance. +:param s: the :any:`Stephen` instance. :type s: Stephen :returns: A :any:`Paths` object containing all words equivalent to @@ -537,7 +707,7 @@ been triggered already). was set with :any:`Stephen.set_word`. .. warning:: - Termination of the Stephen algorithm is undecidable in general, and + Termination of Stephen's procedure is undecidable in general, and this function may never terminate. )pbdoc"); } diff --git a/src/word-graph.cpp b/src/word-graph.cpp index e5de68cb..fe4a8baf 100644 --- a/src/word-graph.cpp +++ b/src/word-graph.cpp @@ -24,7 +24,8 @@ #include // for LIBSEMIGROUPS_EIGEN_ENABLED #include // for operator!=, operator== #include // for IntegralRange<>::value_type -#include // for WordGraph +#include // for v4 word graph helpers +#include // for WordGraph // pybind11.... #include // for self, self_t, operator!=, operator* @@ -1171,17 +1172,22 @@ position in the word reached. m.def( "word_graph_nodes_reachable_from", - [](WordGraph_ const& wg, node_type source) { - return word_graph::nodes_reachable_from(wg, source); + [](WordGraph_ const& wg, + node_type source, + int_or_constant max_depth) { + return v4::word_graph::nodes_reachable_from( + wg, source, to_int(max_depth)); }, py::arg("wg"), py::arg("source"), + py::kw_only(), + py::arg("max_depth") = POSITIVE_INFINITY, R"pbdoc( -:sig=(wg: WordGraph, source: int) -> set[int]: +:sig=(wg: WordGraph, source: int, *, max_depth: int | PositiveInfinity = POSITIVE_INFINITY) -> set[int]: Returns the set of nodes reachable from a given node in a word graph. This function returns a set consisting of all the nodes in the word graph -*wg* that are reachable from *source*. +*wg* that are reachable from *source* by a path of length at most *max_depth*. :param wg: the word graph. :type wg: WordGraph @@ -1189,6 +1195,11 @@ This function returns a set consisting of all the nodes in the word graph :param source: the source node. :type source: int +:param max_depth: + the maximum distance from *source* (default: :any:`POSITIVE_INFINITY`). This + is a keyword-only argument. +:type max_depth: int | PositiveInfinity + :returns: A set consisting of all the nodes in the word graph *wg* that are reachable from *source*. @@ -1196,21 +1207,34 @@ This function returns a set consisting of all the nodes in the word graph :raises LibsemigroupsError: if *source* is out of bounds (greater than or equal to - :any:`WordGraph.number_of_nodes`).)pbdoc"); + :any:`WordGraph.number_of_nodes`). + +.. doctest:: + + >>> from libsemigroups_pybind11 import WordGraph, word_graph + >>> wg = WordGraph(5, [[1], [2], [3], [4], [0]]) + >>> sorted(word_graph.nodes_reachable_from(wg, 0, max_depth=2)) + [0, 1, 2] +)pbdoc"); m.def( "word_graph_number_of_nodes_reachable_from", - [](WordGraph_ const& wg, node_type source) { - return word_graph::number_of_nodes_reachable_from(wg, source); + [](WordGraph_ const& wg, + node_type source, + int_or_constant max_depth) { + return v4::word_graph::number_of_nodes_reachable_from( + wg, source, to_int(max_depth)); }, py::arg("wg"), py::arg("source"), + py::kw_only(), + py::arg("max_depth") = POSITIVE_INFINITY, R"pbdoc( -:sig=(wg: WordGraph, source: int) -> int: +:sig=(wg: WordGraph, source: int, *, max_depth: int | PositiveInfinity = POSITIVE_INFINITY) -> int: Returns the number of nodes reachable from a given node in a word graph. This function returns the number of nodes in the word graph *wg* that are -reachable from *source*. +reachable from *source* by a path of length at most *max_depth*. :param wg: the word graph. :type wg: WordGraph @@ -1218,6 +1242,11 @@ reachable from *source*. :param source: the source node. :type source: int +:param max_depth: + the maximum distance from *source* (default: :any:`POSITIVE_INFINITY`). This + is a keyword-only argument. +:type max_depth: int | PositiveInfinity + :returns: The number of nodes in the word graph *wg* that are reachable from *source*. @@ -1225,7 +1254,15 @@ reachable from *source*. :raises LibsemigroupsError: if *source* is out of bounds (greater than or equal to - :any:`WordGraph.number_of_nodes`).)pbdoc"); + :any:`WordGraph.number_of_nodes`). + +.. doctest:: + + >>> from libsemigroups_pybind11 import WordGraph, word_graph + >>> wg = WordGraph(5, [[1], [2], [3], [4], [0]]) + >>> word_graph.number_of_nodes_reachable_from(wg, 0, max_depth=2) + 3 +)pbdoc"); m.def( "word_graph_random_acyclic", @@ -1259,18 +1296,24 @@ algorithm given in :cite:`Carnino2011`. m.def( "word_graph_spanning_tree", - [](WordGraph_ const& wg, node_type root) { - return word_graph::spanning_tree(wg, root); + [](WordGraph_ const& wg, + node_type root, + int_or_constant max_depth) { + return v4::word_graph::spanning_tree( + wg, root, to_int(max_depth)); }, py::arg("wg"), py::arg("root"), + py::kw_only(), + py::arg("max_depth") = POSITIVE_INFINITY, R"pbdoc( -:sig=(wg: WordGraph, root: int) -> Forest: +:sig=(wg: WordGraph, root: int, *, max_depth: int | PositiveInfinity = POSITIVE_INFINITY) -> Forest: Returns a :any:`Forest` containing a spanning tree of the nodes reachable from a given node in a word graph. This function returns a :any:`Forest` containing a spanning tree of the -nodes reachable from *root* in the word graph *wg*. +nodes reachable from *root* in the word graph *wg* by a path of length at most +*max_depth*. :param wg: the word graph. :type wg: WordGraph @@ -1278,28 +1321,49 @@ nodes reachable from *root* in the word graph *wg*. :param root: the source node. :type root: int +:param max_depth: + the maximum depth of the tree (default: :any:`POSITIVE_INFINITY`). This is a + keyword-only argument. +:type max_depth: int | PositiveInfinity + :returns: A :any:`Forest` object containing a spanning tree. :rtype: Forest :raises LibsemigroupsError: if *root* is out of bounds, i.e. greater than or equal to - :any:`WordGraph.number_of_nodes`.)pbdoc"); + :any:`WordGraph.number_of_nodes`. + +.. doctest:: + + >>> from libsemigroups_pybind11 import UNDEFINED, WordGraph, word_graph + >>> wg = WordGraph(5, [[1], [2], [3], [4], [0]]) + >>> tree = word_graph.spanning_tree(wg, 0, max_depth=2) + >>> tree.parents() + [UNDEFINED, 0, 1] +)pbdoc"); m.def( "word_graph_spanning_tree", - [](WordGraph_ const& wg, node_type root, Forest& f) { - return word_graph::spanning_tree(wg, root, f); + [](WordGraph_ const& wg, + node_type root, + Forest& f, + int_or_constant max_depth) { + return v4::word_graph::spanning_tree( + wg, root, f, to_int(max_depth)); }, py::arg("wg"), py::arg("root"), py::arg("f"), + py::kw_only(), + py::arg("max_depth") = POSITIVE_INFINITY, R"pbdoc( -:sig=(wg: WordGraph, root: int, f: Forest) -> None: +:sig=(wg: WordGraph, root: int, f: Forest, *, max_depth: int | PositiveInfinity = POSITIVE_INFINITY) -> None: Replace the contents of a Forest by a spanning tree of the nodes reachable from a given node in a word graph. This function replaces the content of the :any:`Forest` *f* with a spanning -tree of the nodes reachable from *root* in the word graph *wg*. +tree of the nodes reachable from *root* in the word graph *wg* by a path of +length at most *max_depth*. :param wg: the word graph. :type wg: WordGraph @@ -1310,6 +1374,11 @@ tree of the nodes reachable from *root* in the word graph *wg*. :param f: the Forest object to hold the result. :type f: Forest +:param max_depth: + the maximum depth of the tree (default: :any:`POSITIVE_INFINITY`). This is a + keyword-only argument. +:type max_depth: int | PositiveInfinity + :raises LibsemigroupsError: if *root* is out of bounds, i.e. greater than or equal to :any:`WordGraph.number_of_nodes`.)pbdoc"); diff --git a/tests/test_dot.py b/tests/test_dot.py index 65c8f128..db01f588 100644 --- a/tests/test_dot.py +++ b/tests/test_dot.py @@ -72,6 +72,20 @@ def test_dot_add_node(): assert d.to_string() == 'digraph {\n\n cat\n dog [shape="box"]\n}' +def test_dot_rm_node(): + d = Dot() + d.add_node("cat") + d.add_node("dog") + + assert d.rm_node("cat") is None + assert not d.is_node("cat") + assert d.is_node("dog") + assert d.to_string() == "digraph {\n\n dog\n}" + + with pytest.raises(LibsemigroupsError, match="there is no node named cat to remove"): + d.rm_node("cat") + + def test_dot_add_edge(): d = Dot() d.add_node("cat") diff --git a/tests/test_present.py b/tests/test_present.py index bb431644..ff921502 100644 --- a/tests/test_present.py +++ b/tests/test_present.py @@ -1096,6 +1096,24 @@ def test_helpers_reduce_complements_017(): p.throw_if_bad_alphabet_or_rules() +def test_helpers_normalize_inverse_alphabet(): + p = InversePresentation([5, 7, 3, 9]).inverses([7, 5, 9, 3]) + p.rules = [[5, 3, 7], [9, 5]] + presentation.normalize_alphabet(p) + assert p.alphabet() == [0, 1, 2, 3] + assert p.rules == [[0, 2, 1], [3, 0]] + assert p.inverses() == [1, 0, 3, 2] + p.throw_if_bad_alphabet_rules_or_inverses() + + p = InversePresentation("xXyY").inverses("XxYy") + p.rules = ["xyX", "Yx"] + presentation.normalize_alphabet(p) + assert p.alphabet() == "abcd" + assert p.rules == ["acb", "da"] + assert p.inverses() == "badc" + p.throw_if_bad_alphabet_rules_or_inverses() + + def test_helpers_sort_each_rule_018(): check_sort_each_rule(to_word) check_sort_each_rule(to_string) @@ -1432,7 +1450,36 @@ def test_inverses_039(): check_inverses(to_word) -def test_remove_generator_040(): +def test_inverse_alphabet_040(): + p = InversePresentation([0, 1, 2, 3]) + p.inverses([2, 3, 0, 1]) + assert presentation.inverse_alphabet(p) == [0, 1] + + p.alphabet([0, 2, 1, 3]) + p.inverses([2, 0, 3, 1]) + assert presentation.inverse_alphabet(p) == [0, 1] + + p.alphabet([0, 2, 1, 3]) + p.inverses([2, 0, 1, 3]) + assert presentation.inverse_alphabet(p) == [0, 1, 3] + + p.rules = [[4], [0]] + with pytest.raises(LibsemigroupsError): + presentation.inverse_alphabet(p) + + p = InversePresentation("abAB") + p.inverses("ABab") + assert presentation.inverse_alphabet(p) == "ab" + + p.alphabet("aAbB") + p.inverses("AaBb") + assert presentation.inverse_alphabet(p) == "ab" + + p.inverses("AabB") + assert presentation.inverse_alphabet(p) == "abB" + + +def test_remove_generator_041(): check_remove_generator(to_string) check_remove_generator(to_word) diff --git a/tests/test_stephen.py b/tests/test_stephen.py index 01f6b0f2..0fa2f20d 100644 --- a/tests/test_stephen.py +++ b/tests/test_stephen.py @@ -1594,6 +1594,58 @@ def test_stephen_051(): assert Si == S +@pytest.mark.quick +def test_stephen_052_dot_options_are_optional_keyword_only(): + p = Presentation([0]) + s = Stephen(p) + s.set_word([0]).run() + + expected = """digraph { + + 0 [fontname="STIX Two Text Italic", label="ε", shape="box"] + 1 [fontname="STIX Two Text", label="0", shape="box"] + accept [style="invis"] + initial [style="invis"] + initial -> 0 + 1 -> accept + 0 -> 1 [color="#00ff00", fontname="STIX Two Text", label="0"] +}""" + assert stephen.dot(s).to_string() == expected + assert stephen.dot(s, alphabet=[0]).to_string() == expected + assert stephen.dot(s, alphabet=None, radius=POSITIVE_INFINITY).to_string() == expected + + unfinished = Stephen(p) + unfinished.set_word([0]) + radius_zero = stephen.dot(unfinished, radius=0) + assert [node.name for node in radius_zero.nodes()] == ["0", "accept", "initial"] + + with pytest.raises(TypeError): + stephen.dot(s, [0]) + with pytest.raises(TypeError): + stephen.dot(s, None, 0) + + +@pytest.mark.quick +def test_stephen_053_dot_inverse_options(): + p = InversePresentation("aA") + p.inverses("Aa") + s = Stephen(p) + s.set_word("A").run() + + with_inverse_literals = stephen.dot(s) + without_inverse_literals = stephen.dot(s, use_inverse_literals=False) + assert 'label="a⁻¹"' in with_inverse_literals.to_string() + assert 'label="A"' in without_inverse_literals.to_string() + + unfinished = Stephen(p) + unfinished.set_word("A") + radius_zero = stephen.dot(unfinished, alphabet="a", radius=0, use_inverse_literals=False) + assert [node.name for node in radius_zero.nodes()] == ["0", "accept", "initial"] + + with pytest.raises(TypeError): + stephen.dot(s, "a") + + @pytest.mark.quick def test_stephen_return_policy(): p = InversePresentation("abcABC") diff --git a/tests/test_word_graph.py b/tests/test_word_graph.py index 4ad31a43..28e86da6 100644 --- a/tests/test_word_graph.py +++ b/tests/test_word_graph.py @@ -212,9 +212,13 @@ def test_nodes_reachable_from(word_graphs): assert word_graph.nodes_reachable_from(wg1, 0) == {0, 1, 2, 3, 4} assert word_graph.nodes_reachable_from(wg1, 1) == {1, 2, 3, 4} assert word_graph.nodes_reachable_from(wg1, 3) == {3, 4} + assert word_graph.nodes_reachable_from(wg1, 0, max_depth=0) == {0} + assert word_graph.nodes_reachable_from(wg1, 0, max_depth=2) == {0, 1, 2} with pytest.raises(LibsemigroupsError): word_graph.nodes_reachable_from(wg1, 10) + with pytest.raises(TypeError): + word_graph.nodes_reachable_from(wg1, 0, 2) def test_number_of_nodes_reachable_from(word_graphs): @@ -223,9 +227,13 @@ def test_number_of_nodes_reachable_from(word_graphs): assert word_graph.number_of_nodes_reachable_from(wg1, 0) == 5 assert word_graph.number_of_nodes_reachable_from(wg1, 1) == 4 assert word_graph.number_of_nodes_reachable_from(wg1, 3) == 2 + assert word_graph.number_of_nodes_reachable_from(wg1, 0, max_depth=0) == 1 + assert word_graph.number_of_nodes_reachable_from(wg1, 0, max_depth=2) == 3 with pytest.raises(LibsemigroupsError): word_graph.number_of_nodes_reachable_from(wg1, 10) + with pytest.raises(TypeError): + word_graph.number_of_nodes_reachable_from(wg1, 0, 2) def test_spanning_tree(word_graphs): @@ -239,6 +247,15 @@ def test_spanning_tree(word_graphs): word_graph.spanning_tree(wg1, 0, f) assert word_graph.spanning_tree(wg1, 0) == f + expected = Forest([UNDEFINED, 0, 1], [UNDEFINED, 0, 0]) + assert word_graph.spanning_tree(wg1, 0, max_depth=2) == expected + + word_graph.spanning_tree(wg1, 0, f, max_depth=2) + assert f == expected + + with pytest.raises(TypeError): + word_graph.spanning_tree(wg1, 0, f, 2) + def test_standardize(word_graphs): wg1, _ = word_graphs