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