From f0a12ca5b51e133a8a91c4bd6459547672cf66b3 Mon Sep 17 00:00:00 2001 From: "James D. Mitchell" Date: Mon, 22 Apr 2024 09:43:19 +0100 Subject: [PATCH 1/4] Add v3 support for runner.*pp --- docs/source/knuth-bendix/knuth-bendix.rst | 29 +- docs/source/misc.rst | 6 +- docs/source/reporter.rst | 37 + docs/source/runner.rst | 48 ++ src/knuth-bendix.cpp | 860 +--------------------- src/main.cpp | 37 +- src/main.hpp | 23 +- src/runner.cpp | 480 ++++++++++++ tests/test_runner.py | 46 ++ 9 files changed, 642 insertions(+), 924 deletions(-) create mode 100644 docs/source/reporter.rst create mode 100644 docs/source/runner.rst create mode 100644 src/runner.cpp create mode 100644 tests/test_runner.py diff --git a/docs/source/knuth-bendix/knuth-bendix.rst b/docs/source/knuth-bendix/knuth-bendix.rst index 027ebb78..f013c339 100644 --- a/docs/source/knuth-bendix/knuth-bendix.rst +++ b/docs/source/knuth-bendix/knuth-bendix.rst @@ -48,16 +48,11 @@ Contents KnuthBendixRewriteTrie.confluent_known KnuthBendixRewriteTrie.contains KnuthBendixRewriteTrie.current_state - KnuthBendixRewriteTrie.dead - KnuthBendixRewriteTrie.delta KnuthBendixRewriteTrie.equal_to - KnuthBendixRewriteTrie.finished KnuthBendixRewriteTrie.generating_pairs KnuthBendixRewriteTrie.gilman_graph KnuthBendixRewriteTrie.gilman_graph_node_labels - KnuthBendixRewriteTrie.kill KnuthBendixRewriteTrie.kind - KnuthBendixRewriteTrie.last_report KnuthBendixRewriteTrie.max_overlap KnuthBendixRewriteTrie.max_rules KnuthBendixRewriteTrie.normal_form @@ -67,26 +62,18 @@ Contents KnuthBendixRewriteTrie.number_of_inactive_rules KnuthBendixRewriteTrie.overlap_policy KnuthBendixRewriteTrie.presentation - KnuthBendixRewriteTrie.report_every - KnuthBendixRewriteTrie.report_prefix - KnuthBendixRewriteTrie.report_why_we_stopped - KnuthBendixRewriteTrie.reset_last_report - KnuthBendixRewriteTrie.reset_start_time KnuthBendixRewriteTrie.rewrite - KnuthBendixRewriteTrie.run - KnuthBendixRewriteTrie.run_for - KnuthBendixRewriteTrie.run_until - KnuthBendixRewriteTrie.running - KnuthBendixRewriteTrie.running_for - KnuthBendixRewriteTrie.running_until - KnuthBendixRewriteTrie.start_time - KnuthBendixRewriteTrie.started - KnuthBendixRewriteTrie.stopped - KnuthBendixRewriteTrie.stopped_by_predicate - KnuthBendixRewriteTrie.timed_out KnuthBendixRewriteTrie.total_rules Full API -------- + .. autoclass:: KnuthBendixRewriteTrie :members: + +Methods inherited from Runner +----------------------------- + +.. autoclass:: Runner + :members: + :noindex: diff --git a/docs/source/misc.rst b/docs/source/misc.rst index 70f472f5..9bfeb500 100644 --- a/docs/source/misc.rst +++ b/docs/source/misc.rst @@ -13,6 +13,8 @@ In this section we describe some miscellaneous functionality in .. toctree:: :maxdepth: 1 - overlap report - ukkonen/index + runner + reporter + +.. ukkonen/index diff --git a/docs/source/reporter.rst b/docs/source/reporter.rst new file mode 100644 index 00000000..f88876fd --- /dev/null +++ b/docs/source/reporter.rst @@ -0,0 +1,37 @@ +.. Copyright (c) 2024, J. D. Mitchell + + Distributed under the terms of the GPL license version 3. + + The full license is in the file LICENSE, distributed with this software. + +.. currentmodule:: _libsemigroups_pybind11 + +Reporter +======== + +.. autoclass:: Reporter + :class-doc-from: class + :noindex: + +Contents +-------- + +.. autosummary:: + :nosignatures: + + ~Reporter + Reporter.init + Reporter.last_report + Reporter.report + Reporter.report_every + Reporter.report_prefix + Reporter.reset_last_report + Reporter.reset_start_time + Reporter.start_time + +Full API +-------- + +.. autoclass:: Reporter + :members: + :class-doc-from: init diff --git a/docs/source/runner.rst b/docs/source/runner.rst new file mode 100644 index 00000000..aec53aef --- /dev/null +++ b/docs/source/runner.rst @@ -0,0 +1,48 @@ +.. Copyright (c) 2024, J. D. Mitchell + + Distributed under the terms of the GPL license version 3. + + The full license is in the file LICENSE, distributed with this software. + +.. currentmodule:: _libsemigroups_pybind11 + +Runner +====== + +TODO + +Contents +-------- + +.. autosummary:: + :nosignatures: + + ~Runner + Runner.current_state + Runner.dead + Runner.finished + Runner.kill + Runner.run + Runner.run_for + Runner.running + Runner.running_for + Runner.running_until + Runner.started + Runner.state + Runner.stopped + Runner.stopped_by_predicate + Runner.timed_out + +Full API +-------- + +.. autoclass:: Runner + :members: + :class-doc-from: class + +Methods inherited from Reporter +------------------------------- + +.. autoclass:: Reporter + :members: + :noindex: diff --git a/src/knuth-bendix.cpp b/src/knuth-bendix.cpp index 118b3434..542ed7f7 100644 --- a/src/knuth-bendix.cpp +++ b/src/knuth-bendix.cpp @@ -45,7 +45,7 @@ #include // for PyUnicode_DecodeLatin1 // libsemigroups_pybind11.... -#include "doc-strings.hpp" // for dead, finished, kill, report +#include "doc-strings.hpp" // for init_knuth_bendix #include "main.hpp" // for init_knuth_bendix namespace py = pybind11; @@ -76,7 +76,7 @@ namespace libsemigroups { R"pbdoc( Return the number of pending rules that must accumulate before they are reduced, processed, and added to the system. - + The default value is ``128``. A value of ``1`` means :py:meth:`run` should attempt to add each rule as they are created without waiting for rules to accumulate. @@ -85,7 +85,7 @@ accumulate. :return: The batch size. :rtype: int -.. seealso:: :py:meth:`run`. +.. seealso:: :py:meth:`run`. )pbdoc") .def("batch_size", py::overload_cast(&KnuthBendix::batch_size), @@ -94,7 +94,7 @@ accumulate. Specify the number of pending rules that must accumulate before they are reduced, processed, and added to the system. -The default value is ``128``, and should be set to ``1`` if :py:meth:`run` +The default value is ``128``, and should be set to ``1`` if :py:meth:`run` should attempt to add each rule as they are created without waiting for rules to accumulate. @@ -102,7 +102,7 @@ to accumulate. :type val: int :return: A reference to ``self``. -.. seealso:: :py:meth:`run`. +.. seealso:: :py:meth:`run`. )pbdoc") .def("check_confluence_interval", py::overload_cast<>( @@ -319,7 +319,7 @@ which represent the rewriting rules. The first entry in every such pair is greater than the second according to the reduction ordering of the :py:class:`KnuthBendix` instance. The rules are sorted according to the reduction ordering used by the rewriting system, on the first -entry. +entry. :Parameters: None :return: A copy of the currently active rules @@ -360,7 +360,7 @@ Check if the current system knows the state of confluence of the current rules. :Parameters: None :return: ``True`` if the confluence of the rules in the :py:class:`KnuthBendix` instance is known, and - ``False`` if it is not. + ``False`` if it is not. :rtype: bool )pbdoc") .def( @@ -384,7 +384,7 @@ The semigroup is finite if the graph is cyclic, and infinite otherwise. :Parameters: None :return: The Gilman :py:class:`WordGraph`. :rtype: WordGraph - + .. warning:: This will terminate when the :py:class:`KnuthBendix` instance is reduced and confluent, which might be never. @@ -457,74 +457,6 @@ The handedness of the congruence (left, right, or 2-sided). &libsemigroups::KnuthBendix::add_pair)) .def("generating_pairs", &KnuthBendix::generating_pairs); ////////////////////////////////////////////////////////////////////////// - // Inherited from Runner - ////////////////////////////////////////////////////////////////////////// - kb.def("run", &KnuthBendix::run, runner_doc_strings::run) - .def( - "run_for", - [](KnuthBendix& kb, std::chrono::nanoseconds t) { - kb.run_for(t); - }, - runner_doc_strings::run_for) - .def("timed_out", - &KnuthBendix::timed_out, - runner_doc_strings::timed_out) - .def( - "run_until", - [](KnuthBendix& kb, std::function& f) { - kb.run_until(f); - }, - runner_doc_strings::run_until) - .def("report_why_we_stopped", - &KnuthBendix::report_why_we_stopped, - runner_doc_strings::report_why_we_stopped) - .def("finished", - &KnuthBendix::finished, - runner_doc_strings::finished) - .def("started", - &KnuthBendix::started, - runner_doc_strings::started) - .def("running", - &KnuthBendix::running, - runner_doc_strings::running) - .def("kill", &KnuthBendix::kill, runner_doc_strings::kill) - .def("dead", &KnuthBendix::dead, runner_doc_strings::dead) - .def("stopped", - &KnuthBendix::stopped, - runner_doc_strings::stopped) - .def("stopped_by_predicate", - &KnuthBendix::stopped_by_predicate, - runner_doc_strings::stopped_by_predicate) - .def("running_for", - &KnuthBendix::running_for, - runner_doc_strings::running_for) - .def("running_until", - &KnuthBendix::running_until, - runner_doc_strings::running_until) - .def("current_state", - &KnuthBendix::current_state, - runner_doc_strings::current_state); - ////////////////////////////////////////////////////////////////////////// - // Inherited from Reporter - ////////////////////////////////////////////////////////////////////////// - kb.def("report_every", - [](KnuthBendix& kb, std::chrono::nanoseconds val) { - kb.report_every(val); - }) - .def("report_every", - [](KnuthBendix& kb) { return kb.report_every(); }) - .def("start_time", &KnuthBendix::start_time) - .def("delta", &KnuthBendix::delta) - .def("reset_start_time", &KnuthBendix::reset_start_time) - .def("last_report", &KnuthBendix::last_report) - .def("reset_last_report", &KnuthBendix::reset_last_report) - .def("report_prefix", - [](KnuthBendix kb, std::string const& val) { - kb.report_prefix(val); - }) - .def("report_prefix", - [](KnuthBendix kb) { return kb.report_prefix(); }); - ////////////////////////////////////////////////////////////////////////// // Helpers ////////////////////////////////////////////////////////////////////////// m.def("by_overlap_length", [](KnuthBendix& kb) { @@ -603,780 +535,4 @@ is returned. bind_knuth_bendix(m, "KnuthBendixRewriteFromLeft"); bind_knuth_bendix(m, "KnuthBendixRewriteTrie"); } - - /* - .def("::libsemigroups::Congruence", - &libsemigroups::CongruenceInterface::libsemigroups::Congruence, - R"pbdoc( - - - )pbdoc") -.def(py::init<>()) -.def("init", - py::overload_cast<>(&libsemigroups::KnuthBendix::init), - R"pbdoc( - - - )pbdoc") -.def("~CongruenceInterface", - &libsemigroups::KnuthBendix::~CongruenceInterface, - R"pbdoc( - - - )pbdoc") -.def("number_of_classes", - &libsemigroups::KnuthBendix::number_of_classes, - R"pbdoc( - - - :return: The number of congruences classes of this if this number -is finite, or POSITIVE_INFINITY in some cases if this number is not finite. - )pbdoc") -.def("contains", - &libsemigroups::KnuthBendix::contains, - R"pbdoc( - - - )pbdoc") -.def("validate_word", - &libsemigroups::KnuthBendix::validate_word, - R"pbdoc( - - - )pbdoc") -.def("kind", - py::overload_cast<>(&libsemigroups::KnuthBendix::kind, py::const_), - R"pbdoc( - - - - :Returns: A congruence_kind. - )pbdoc") -.def("number_of_generating_pairs", - &libsemigroups::KnuthBendix::number_of_generating_pairs, - R"pbdoc( - - - :return: A const_iterator pointing to a relation_type. - )pbdoc") -.def("generating_pairs", - &libsemigroups::KnuthBendix::generating_pairs, - R"pbdoc( - - - )pbdoc") -; - */ - - /* - class FroidurePinBase; - - namespace { - std::string to_latin1(std::string const& u) { - static py::object bytes; - static bool first_call = true; - if (first_call) { - first_call = false; - try { - bytes = py::getattr(py::globals()["__builtins__"], "bytes"); - } catch (py::error_already_set& e) { - bytes = py::globals()["__builtins__"]["bytes"]; - } - } - return PyBytes_AS_STRING(bytes(py::str(u), "latin1").ptr()); - } - - py::str from_latin1(std::string const& u) { - // TODO don't pass NULL as the final param - return py::reinterpret_steal( - PyUnicode_DecodeLatin1(u.data(), u.length(), NULL)); - } - } // namespace - */ - /* - using rule_type = FpSemigroupInterface::rule_type; - - m.def("redundant_rule_strings", - [](Presentation& p, std::chrono::nanoseconds t) { - return std::distance(p.rules.cbegin(), - presentation::redundant_rule(p, t)); - }); - m.def("redundant_rule_words", - [](Presentation& p, std::chrono::nanoseconds t) { - return std::distance(p.rules.cbegin(), - presentation::redundant_rule(p, t)); - }); - - py::class_> - kb(m, "KnuthBendix"); - - kb.def(py::init<>(), - R"pbdoc( - Default constructor. - )pbdoc") - .def("__repr__", - [](fpsemigroup::KnuthBendix const& kb) { - auto n = (kb.alphabet().empty() - ? "-" - : detail::to_string(kb.alphabet().size())); - auto conf = (kb.confluent() ? "" : "non-"); - return std::string("<") + conf + "confluent KnuthBendix with " - + n + " letters + " - + detail::to_string(kb.number_of_active_rules()) - + " active rules>"; - }) - .def( - "set_alphabet", - [](fpsemigroup::KnuthBendix& kb, std::string const& a) { - kb.set_alphabet(to_latin1(a)); - }, - py::arg("a"), - R"pbdoc( - Set the alphabet of the finitely presented semigroup. - - :Parameters: **a** (str) - the alphabet. - - :Returns: None - )pbdoc") - .def("set_alphabet", - py::overload_cast(&fpsemigroup::KnuthBendix::set_alphabet), - py::arg("n"), - R"pbdoc( - Set the size of the alphabet. - - :Parameters: **n** (int) - the number of letters. - :Returns: None - )pbdoc") - .def( - "alphabet", - [](fpsemigroup::KnuthBendix const& kb) -> py::str { - return from_latin1(kb.alphabet()); - }, - R"pbdoc( - Returns the alphabet. - - :Parameters: None - :Returns: A string. - )pbdoc") - .def( - "alphabet", - [](fpsemigroup::KnuthBendix const& kb, size_t i) -> py::str { - return from_latin1(kb.alphabet(i)); - }, - py::arg("i"), - R"pbdoc( - Returns the i-th letter of the alphabet of the finitely presented - semigroup represented by this. - - :Parameters: **i** (int) - the index of the letter. - - :Returns: A string. - )pbdoc") - .def( - "add_rule", - [](fpsemigroup::KnuthBendix& kb, - std::string const& u, - std::string const& v) -> void { - kb.add_rule(to_latin1(u), to_latin1(v)); - }, - py::arg("u"), - py::arg("v"), - R"pbdoc( - Add a rule. - - :Parameters: - **u** (str) - the left-hand side of the rule - being added. - - **v** (str) - the right-hand side of the rule - being added. - - :Returns: None - )pbdoc") - .def("size", - &fpsemigroup::KnuthBendix::size, - R"pbdoc( - Returns the size of the finitely presented semigroup. - - :Parameters: None - - :return: A ``int`` or :py:obj:`POSITIVE_INFINITY`. - )pbdoc") - .def("number_of_active_rules", - &fpsemigroup::KnuthBendix::number_of_active_rules, - R"pbdoc( - Returns the current number of active rules in the KnuthBendix - instance. - - :Parameters: None - :return: An ``int``. - )pbdoc") - .def("confluent", - &fpsemigroup::KnuthBendix::confluent, - R"pbdoc( - Check if the KnuthBendix instance is confluent. - - :Parameters: None - - :return: - ``True`` if the KnuthBendix instance is confluent and - ``False`` if it is not. - )pbdoc") - .def( - "active_rules", - [](fpsemigroup::KnuthBendix const& kb) { - auto result = kb.active_rules(); - std::for_each(result.begin(), result.end(), [](rule_type& rule) { - rule.first = from_latin1(rule.first); - rule.second = from_latin1(rule.second); - }); - return result; - }, - R"pbdoc( - Returns a copy of the active rules of the KnuthBendix instance. - - :Parameters: None - - :return: A copy of the currently active rules. - )pbdoc") - .def( - "number_of_rules", - [](fpsemigroup::KnuthBendix const& kb) { - return kb.number_of_rules(); - }, - R"pbdoc( - Returns the number of rules of the finitely presented semigroup. - - :Parameters: None - :return: A ``int``. - )pbdoc") - .def("set_identity", - py::overload_cast( - &fpsemigroup::KnuthBendix::set_identity), - py::arg("id"), - R"pbdoc( - Set a character in alphabet() to be the identity using its index. - - :Parameters: **id** (int) - the index of the character to be the - identity. - - :Returns: None - )pbdoc") - .def( - "set_identity", - [](fpsemigroup::KnuthBendix& kb, std::string const& id) { - kb.set_identity(to_latin1(id)); - }, - py::arg("id"), - R"pbdoc( - Set a character in alphabet() to be the identity. - - :Parameters: **id** (str) - a string containing the character to - be the identity. - - :Returns: None - )pbdoc") - .def( - "identity", - [](fpsemigroup::KnuthBendix const& kb) { - return from_latin1(kb.identity()); - }, - R"pbdoc( - Returns the identity of this, or raises an exception if there - isn't one. - - :Parameters: None - :return: A string of length 1. - )pbdoc") - .def( - "set_inverses", - [](fpsemigroup::KnuthBendix& kb, std::string const& a) { - return kb.set_inverses(to_latin1(a)); - }, - py::arg("a"), - R"pbdoc( - Set the inverses of letters in alphabet(). - - :param a: a string containing the inverses of the generators. - :type a: str - - :return: None - )pbdoc") - .def( - "inverses", - [](fpsemigroup::KnuthBendix const& kb) { - return from_latin1(kb.inverses()); - }, - R"pbdoc( - Returns the inverses of this, or raises an exception if there - aren't any. - - :Parameters: None - :return: A ``str``. - )pbdoc") - .def("is_obviously_finite", - &fpsemigroup::KnuthBendix::is_obviously_finite, - R"pbdoc( - Return ``True`` if the finitely presented semigroup represented - by this is obviously finite, and ``False`` if it is not obviously finite. - - :return: A ``bool``. - )pbdoc") - .def("is_obviously_infinite", - &fpsemigroup::KnuthBendix::is_obviously_infinite, - R"pbdoc( - Returns ``True`` if the finitely presented semigroup represented - by this is obviously infinite, and ``False`` if it is not obviously - infinite. - - :return: A bool. - )pbdoc") - .def( - "equal_to", - [](fpsemigroup::KnuthBendix& kb, - std::string const& u, - std::string const& v) { - return kb.equal_to(to_latin1(u), to_latin1(v)); - }, - py::arg("u"), - py::arg("v"), - R"pbdoc( - Check if two words represent the same element. - - :Parameters: - **u** (str) - first word for comparison. - - **v** (str) - second word for comparison. - - :Returns: - ``True`` if the strings ``u`` and ``v`` represent the same - element of the finitely presented semigroup, and ``False`` - otherwise. - )pbdoc") - .def("equal_to", - py::overload_cast( - &fpsemigroup::KnuthBendix::equal_to), - py::arg("u"), - py::arg("v"), - R"pbdoc( - Check if two words represent the same element. - - :Parameters: - **u** (List[int]) - first word for comparison. - - **v** (List[int]) - second word for comparison. - - :Returns: - ``True`` if the words ``u`` and ``v`` represent the same - element of the finitely presented semigroup, and ``False`` - otherwise. - )pbdoc") - .def( - "normal_form", - [](fpsemigroup::KnuthBendix& kb, std::string const& w) { - return from_latin1(kb.normal_form(to_latin1(w))); - }, - py::arg("w"), - R"pbdoc( - Returns a normal form for a string. - - :Parameters: **w** (str) - the word whose normal form we want to - find. - - :Returns: A ``str``. - )pbdoc") - .def("normal_form", - py::overload_cast( - &fpsemigroup::KnuthBendix::normal_form), - py::arg("w"), - R"pbdoc( - Returns a normal form for a word. - - :Parameters: **w** (List[int]) - the word whose normal form we - want to find. - - :Returns: - The normal form of the parameter ``w``. - )pbdoc") - .def("add_rule", - py::overload_cast( - &fpsemigroup::KnuthBendix::add_rule), - py::arg("u"), - py::arg("v"), - R"pbdoc( - Add a rule. - - :Parameters: - **u** (List[int]) - the left-hand side of the - rule. - - **v** (List[int]) - the right-hand side of the - rule. - - :Returns: None - )pbdoc") - .def("contains_empty_string", - &fpsemigroup::KnuthBendix::contains_empty_string, - R"pbdoc( - Returns whether or not the empty string belongs to the finitely - presented semigroup represented by this. - - :return: A ``bool``. - )pbdoc") - .def("number_of_normal_forms", - &fpsemigroup::KnuthBendix::number_of_normal_forms, - py::arg("min"), - py::arg("max"), - R"pbdoc( - Returns the number of normal forms with length in a given range. - - :param min: the minimum length of a normal form to count - :type min: int - :param max: - one larger than the maximum length of a normal form to count. - :type max: int - - :return: An ``int``. - )pbdoc") - .def("validate_letter", - py::overload_cast(&fpsemigroup::KnuthBendix::validate_letter, - py::const_), - py::arg("c"), - R"pbdoc( - Validates a letter. - - :Parameters: **c** (str) - the letter to validate. - - :Returns: None - )pbdoc") - .def("validate_letter", - py::overload_cast( - &fpsemigroup::KnuthBendix::validate_letter, py::const_), - py::arg("c"), - R"pbdoc( - Validates a letter. - - :Parameters: **c** (int) - the letter to validate. - - :Returns: None - )pbdoc") - .def( - "validate_word", - [](fpsemigroup::KnuthBendix const& kb, std::string const& w) { - kb.validate_word(to_latin1(w)); - }, - py::arg("w"), - R"pbdoc( - Validates a word. - - :Parameters: **w** (str) - the word to validate. - - :Returns: None - )pbdoc") - .def("validate_word", - py::overload_cast( - &fpsemigroup::KnuthBendix::validate_word, py::const_), - py::arg("w"), - R"pbdoc( - Validates a word. - - This function checks that the word ``w`` is defined over the - same alphabet as an instance of :py:class:`KnuthBendix`. - - :Parameters: **w** (List[int]) - the word to validate. - - :Returns: None - )pbdoc") - .def( - "froidure_pin", - [](fpsemigroup::KnuthBendix& x) { return x.froidure_pin(); }, - R"pbdoc( - Returns a :py:class:`FroidurePin` instance isomorphic to the - finitely presented semigroup defined by this. - - :Parameters: None - - :return: A :py:class:`FroidurePin`. - )pbdoc") - .def( - "has_froidure_pin", - [](fpsemigroup::KnuthBendix const& x) { - return x.has_froidure_pin(); - }, - R"pbdoc( - Returns ``True`` if a :py:class:`FroidurePin` instance isomorphic - to the finitely presented semigroup defined by this has already - been computed, and ``False`` if not. - - :Parameters: None - - :return: A ``bool``. - )pbdoc") - .def("run_for", - (void(fpsemigroup::KnuthBendix:: // NOLINT(whitespace/parens) - *)(std::chrono::nanoseconds)) - & Runner::run_for, - py::arg("t"), - runner_doc_strings::run_for) - .def("run_until", - (void(fpsemigroup::KnuthBendix:: // NOLINT(whitespace/parens) - *)(std::function&)) - & Runner::run_until, - py::arg("func"), - runner_doc_strings::run_until) - .def("run", &fpsemigroup::KnuthBendix::run, runner_doc_strings::run) - .def("kill", &fpsemigroup::KnuthBendix::kill, runner_doc_strings::kill) - .def("dead", &fpsemigroup::KnuthBendix::dead, runner_doc_strings::dead) - .def("finished", - &fpsemigroup::KnuthBendix::finished, - runner_doc_strings::finished) - .def("started", - &fpsemigroup::KnuthBendix::started, - runner_doc_strings::started) - .def( - "running", - [](fpsemigroup::KnuthBendix const& kb) { return kb.running(); }, - runner_doc_strings::running) - .def("timed_out", - &fpsemigroup::KnuthBendix::timed_out, - runner_doc_strings::timed_out) - .def("stopped", - &fpsemigroup::KnuthBendix::stopped, - runner_doc_strings::stopped) - .def("stopped_by_predicate", - &fpsemigroup::KnuthBendix::stopped_by_predicate, - runner_doc_strings::stopped_by_predicate) - .def("report", - &fpsemigroup::KnuthBendix::report, - runner_doc_strings::report) - .def("report_every", - (void(fpsemigroup::KnuthBendix:: // NOLINT(whitespace/parens) - *)(std::chrono::nanoseconds)) - & Runner::report_every, - py::arg("t"), - runner_doc_strings::report_every) - .def("report_why_we_stopped", - &fpsemigroup::KnuthBendix::report_why_we_stopped, - runner_doc_strings::report_why_we_stopped) - .def("char_to_uint", - &fpsemigroup::KnuthBendix::char_to_uint, - py::arg("a"), - R"pbdoc( - Convert a single letter ``string`` to a ``int`` representing the - same generator. - - :param a: the string to convert. - :type a: str - - :return: an ``int``. - )pbdoc") - .def("uint_to_char", - &fpsemigroup::KnuthBendix::uint_to_char, - py::arg("a"), - R"pbdoc( - Convert an ``int`` to a ``char`` representing the - same generator of the finitely presented semigroup represented - by this. - - :param a: the letter to convert. - :type a: int - - :return: A ``str``. - )pbdoc") - .def( - "string_to_word", - [](fpsemigroup::KnuthBendix const& kb, std::string const& w) { - return kb.string_to_word(to_latin1(w)); - }, - py::arg("w"), - R"pbdoc( - Convert a string to a list of ``int`` representing the same - element of the finitely presented semigroup represented by this. - - :param w: the string to convert. - :type w: str - - :return: a ``List[int]``. - )pbdoc") - .def( - "word_to_string", - [](fpsemigroup::KnuthBendix const& kb, word_type const& w) { - return from_latin1(kb.word_to_string(w)); - }, - py::arg("w"), - R"pbdoc( - Convert a list of ``int`` to a string representing the same - element of the finitely presented semigroup represented by this. - - :param w: the list to convert. - :type w: List[int] - - :return: A string. - )pbdoc") - .def("to_gap_string", - &fpsemigroup::KnuthBendix::to_gap_string, - R"pbdoc( - Returns a string containing GAP commands for defining a finitely - presented semigroup equal to that represented by this. - - :Parameters: None - - :return: A string. - )pbdoc") - .def("check_confluence_interval", - &fpsemigroup::KnuthBendix::check_confluence_interval, - py::arg("val"), - R"pbdoc( - Set the interval at which confluence is checked. - - :param val: the new value of the interval. - :type val: int - - :return: ``self``. - )pbdoc") - .def("overlap_policy", - &fpsemigroup::KnuthBendix::overlap_policy, - py::arg("val"), - R"pbdoc( - Set the overlap policy. - - :param val: the maximum number of rules. - :type val: int - - :return: ``self``. - )pbdoc") - .def("max_overlap", - &fpsemigroup::KnuthBendix::max_overlap, - py::arg("val"), - R"pbdoc( - Set the maximum length of overlaps to be considered. - - :param val: the new value of the maximum overlap length. - :type val: int - - :return: ``self``. - )pbdoc") - .def("max_rules", - &fpsemigroup::KnuthBendix::max_rules, - py::arg("val"), - R"pbdoc( - Set the maximum number of rules. - - :param val: the maximum number of rules. - :type val: int - - :return: ``self``. - )pbdoc") - .def("knuth_bendix_by_overlap_length", - &fpsemigroup::KnuthBendix::knuth_bendix_by_overlap_length, - R"pbdoc( - Run the Knuth-Bendix algorithm by overlap length. - - :Parameters: None - - :return: None - )pbdoc") - .def( - "rules", - [](fpsemigroup::KnuthBendix const& kb) { - return py::make_iterator(kb.cbegin_rules(), kb.cend_rules()); - }, - R"pbdoc( - Returns an iterator to the generating pairs of the congruence. - )pbdoc") - .def( - "normal_forms", - [](fpsemigroup::KnuthBendix& kb, size_t const mn, size_t const mx) { - return py::make_iterator(kb.cbegin_normal_forms(mn, mx), - kb.cend_normal_forms()); - }, - py::arg("mn"), - py::arg("mx"), - R"pbdoc( - Returns an iterator to the normal forms with length in the given - range. - - :param mn: the minimum length. - :type mn: int - :param mx: the maximum length. - :type mx: int - - :return: An iterator. - )pbdoc") - .def( - "normal_forms_alphabet", - [](fpsemigroup::KnuthBendix& kb, - std::string const& lphbt, - size_t const mn, - size_t const mx) { - return py::make_iterator(kb.cbegin_normal_forms(lphbt, mn, mx), - kb.cend_normal_forms()); - }, - py::arg("lphbt"), - py::arg("mn"), - py::arg("mx"), - R"pbdoc( - Returns an iterator to the normal forms of the congruence using - the specified alphabet, and with length in the given range. - - :param lphbt: the alphabet. - :type lphbt: str - :param mn: the minimum length. - :type mn: int - :param mx: the maximum length. - :type mx: int - - :return: An iterator. - )pbdoc") - .def("add_rule", - py::overload_cast(&fpsemigroup::KnuthBendix::add_rule), - py::arg("rel"), - R"pbdoc( - Add a rule. - - :Parameters: **rel** (Tuple[str, str]) - the rule being added. - - :Returns: None - )pbdoc") - .def("add_rules", - py::overload_cast const&>( - &fpsemigroup::KnuthBendix::add_rules), - py::arg("rels"), - R"pbdoc( - Add the rules in a list. - - :Parameters: **rels** (List[Tuple[str, str]]) - list of rules to - add. - - :Returns: None - )pbdoc") - .def("add_rules", - py::overload_cast( - &fpsemigroup::KnuthBendix::add_rules), - py::arg("rels"), - R"pbdoc( - Add the rules in a ``FroidurePin`` instance. - - :Parameters: **rels** (FroidurePin) - the instance. - - :Returns: None - )pbdoc") - .def("gilman_digraph", - &fpsemigroup::KnuthBendix::gilman_digraph, - py::return_value_policy::copy, - R"pbdoc( - Returns the associated Gilman digraph (or automata). - - :Parameters: None - - :return: A copy of an :py:class:`ActionDigraph`. - )pbdoc") - .def("rewrite", - py::overload_cast(&fpsemigroup::KnuthBendix::rewrite, - py::const_), - R"pbdoc( - Rewrite a word. - - Rewrites a copy of the string ``w`` rewritten according to the - current rules in the ``KnuthBendix`` instance. - - :param w: the word to rewrite. - :type w: str - - :returns: A copy of the argument ``w`` after it has been - rewritten. )pbdoc"); } // namespace - */ } // namespace libsemigroups diff --git a/src/main.cpp b/src/main.cpp index 336cc42a..f9806530 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -117,16 +117,17 @@ namespace libsemigroups { m.attr("UNDEFINED") = UNDEFINED; //////////////////////////////////////////////////////////////////////// - // Abstract classes that are required by other classes + // Classes //////////////////////////////////////////////////////////////////////// - py::class_(m, "Reporter"); - py::class_(m, "Runner"); - py::class_( - m, "CongruenceInterface"); + + init_reporter(m); + init_runner(m); //////////////////////////////////////////////////////////////////////// - // Classes + // Abstract classes that are required by other classes //////////////////////////////////////////////////////////////////////// + py::class_( + m, "CongruenceInterface"); init_forest(m); init_gabow(m); @@ -414,28 +415,4 @@ default. //////////////////////////////////////////////////////////////////////// // Init //////////////////////////////////////////////////////////////////////// - /* - init_action_digraph(m); - init_bipart(m); - init_bmat8(m); - init_cong(m); - init_forest(m); - init_fpsemi_examples(m); - init_fpsemi(m); - init_kambites(m); - init_matrix(m); - init_pbr(m); - init_present(m); - init_sims1(m); - init_stephen(m); - init_todd_coxeter(m); - init_transf(m); - init_ukkonen(m); - - // Must come last - init_froidure_pin(m); - init_konieczny(m); - - } - */ } // namespace libsemigroups diff --git a/src/main.hpp b/src/main.hpp index 85576b37..832a41a5 100644 --- a/src/main.hpp +++ b/src/main.hpp @@ -25,6 +25,10 @@ namespace libsemigroups { namespace py = pybind11; + void init_reporter(py::module&); + void init_runner(py::module&); + void init_imagerightaction(py::module&); + void init_forest(py::module&); void init_gabow(py::module&); void init_knuth_bendix(py::module&); @@ -35,25 +39,6 @@ namespace libsemigroups { void init_words(py::module&); void init_word_graph(py::module&); - /* - void init_bipart(py::module&); - void init_bmat8(py::module&); - void init_cong(py::module&); - void init_forest(py::module&); - void init_fpsemi(py::module&); - void init_fpsemi_examples(py::module&); - void init_froidure_pin(py::module&); - void init_kambites(py::module&); - void init_konieczny(py::module&); - void init_matrix(py::module&); - void init_pbr(py::module&); - void init_present(py::module&); - void init_sims1(py::module&); - void init_stephen(py::module&); - void init_todd_coxeter(py::module&); - void init_transf(py::module&); - void init_ukkonen(py::module&); -*/ } // namespace libsemigroups #endif // SRC_MAIN_HPP_ diff --git a/src/runner.cpp b/src/runner.cpp new file mode 100644 index 00000000..cb994cd6 --- /dev/null +++ b/src/runner.cpp @@ -0,0 +1,480 @@ +// +// libsemigroups_pybind11 +// Copyright (C) 2024 James D. Mitchell +// +// 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 . +// + +// libsemigroups headers +#include + +// pybind11.... +#include +#include +#include +#include + +// libsemigroups_pybind11.... +#include "main.hpp" // for init_reporter, init_runner + +namespace py = pybind11; + +namespace libsemigroups { + + void init_reporter(py::module& m) { + m.def("delta", + &delta, + py::arg("t"), + R"pbdoc( +The time between the given point and now. + +:param t: + the time point + +:type t: + datetime.timedelta + +:returns: + The difference between the time point ``t`` and now. + +:rtype: + datetime.timedelta +)pbdoc"); + + py::class_ thing(m, + "Reporter", + R"pbdoc( +Collection of values related to reporting. + +This class exists to collect some values related to reporting in its derived +classes. These values are: + +- :any:`report_prefix()`; +- :any:`report_every()`; +- :any:`last_report()`; +- :any:`start_time()`. +)pbdoc"); + + thing.def(py::init<>(), R"pbdoc( +Default construct a :any:`Reporter` object such that the following hold: + +- :any:`report_prefix()` is empty; +- :any:`report_every()` is set to 1 second; +- :any:`last_report()` is now; +- :any:`start_time()` is now. +)pbdoc"); + thing.def(py::init(), R"pbdoc( +Default copy constructor. +)pbdoc"); + thing.def("init", + &Reporter::init, + R"pbdoc( +Initialize an existing Reporter object. + +This function puts a :any:`Reporter` object back into the same state as +if it had been newly default constructed. + +:returns: ``self``. +:rtype: Reporter + +.. seealso:: :any:`Reporter()` +)pbdoc"); + thing.def("report", + &Reporter::report, + R"pbdoc( +Check if it is time to report. + +This function can be used to check if enough time has passed that we should +report again. That is if the time between :any:`last_report()` and now is +greater than the value of :any:`report_every()`. If ``True`` is returned, then +:any:`last_report()` is set to now. + +:returns: A value of type ``bool``. +:rtype: bool + +.. seealso:: :any:`report_every`. +)pbdoc"); + thing.def( + "report_every", + [](Reporter& self, Reporter::nanoseconds val) { + return self.report_every(val); + }, + py::arg("val"), + R"pbdoc( +Set the minimum elapsed time between reports. + +This function can be used to specify at run time the minimum elapsed time between two calls to :any:`report()` that will return ``True`` . If :any:`report()` returns ``True`` at time ``s`` , then :any:`report()` will only return ``True`` again after at least time ``s + t`` has elapsed. + +:param val: the amount of time between reports. +:type val: datetime.timedelta + +:returns: ``self``. +:rtype: Reporter + +.. seealso:: :any:`report_every` +)pbdoc"); + thing.def( + "report_every", + [](Reporter const& self) { return self.report_every(); }, + R"pbdoc( +Get the minimum elapsed time between reports. + +:returns: + The time delta between reports. + +:rtype: + datetime.timedelta +)pbdoc"); + thing.def("start_time", + &Reporter::start_time, + R"pbdoc( +Get the start time. + +This is the time point at which +:any:`reset_start_time()` was last called, which is also the time of +construction of a :any:`Reporter` instance if :any:`reset_start_time()` +is not explicitly called. + +:returns: + The time delta representing the start time. + +:rtype: + datetime.timedelta +)pbdoc"); + thing.def("reset_start_time", + &Reporter::reset_start_time, + R"pbdoc( +Reset the start time (and last report) to now. + +:returns: ``self``. +:rtype: Reporter +)pbdoc"); + thing.def("last_report", + &Reporter::last_report, + R"pbdoc( +Get the time point of the last report. Returns the time point of the +last report, as set by one of: + +- :any:`reset_start_time()`; +- :any:`report_every()`; or +- :any:`report()`. + +:returns: + A :any:`datetime.timedelta`. + +:rtype: + datetime.timedelta +)pbdoc"); + thing.def("reset_last_report", + &Reporter::reset_last_report, + R"pbdoc( +Set the last report time point to now. + +:returns: ``self``. + +:rtype: + Reporter +)pbdoc"); + thing.def( + "report_prefix", + [](Reporter& self, std::string const& val) { + return self.report_prefix(val); + }, + py::arg("val"), + R"pbdoc( +Set the prefix string for reporting. + +This function sets the return value of :any:`report_prefix()` to (a copy of) the argument ``val`` . Typically this prefix should be the name of the algorithm being run at the outmost level. + +:param val: the new value of the report prefix. +:type val: str + +:returns: ``self``. +:rtype: Reporter +)pbdoc"); + thing.def( + "report_prefix", + [](Reporter const& self) { return self.report_prefix(); }, + R"pbdoc( +Get the current prefix string for reporting. This function gets the +current value of the prefix string for reporting (set via +:any:`report_prefix`), which is typically the name of the +algorithm being run at the outmost level. + +:returns: + The prefix string. + +:rtype: + str +)pbdoc"); + } // init_reporter + + void init_runner(py::module& m) { + py::class_ thing(m, + "Runner", + R"pbdoc( +Abstract class for derived classes that run an algorithm. + +Many of the classes in ``libsemigroups`` implementing the algorithms, that +are the reason for the existence of this library, are derived from +:any:`Runner` . The :any:`Runner` class exists to collect various common +tasks required by such a derived class with a possibly long running +:any:`run` . These common tasks include: + +* running for a given amount of time (:any:`run_for`) +* running until a nullary predicate is true (:any:`run_until`) +* checking the status of the algorithm: has it :any:`started` ? :any:`finished`? been killed by another thread (:any:`dead`)? has it timed out (:any:`timed_out`)? has it :any:`stopped` for any reason? +* permit the function :any:`run` to be killed from another thread (:any:`kill`). +)pbdoc"); + + py::enum_ state(m, "Runner.state", R"pbdoc( +Indicates that none of :any:`run` , :any:`run_for` , or :any:`run_until` +has been called since construction or the last call to :any:`init`. +)pbdoc"); + state + .value("never_run", Runner::state::never_run, R"pbdoc( +Indicates that none of :any:`run` , :any:`run_for` , or :any:`run_until` +has been called since construction or the last call to :any:`init`. +)pbdoc") + .value("running_to_finish", Runner::state::running_to_finish, R"pbdoc( +Indicates that none of :any:`run` , :any:`run_for` , or :any:`run_until` +has been called since construction or the last call to :any:`init`. +)pbdoc") + .value("running_for", Runner::state::running_for, R"pbdoc( +Indicates that none of :any:`run` , :any:`run_for` , or :any:`run_until` +has been called since construction or the last call to :any:`init`. +)pbdoc") + .value("running_until", Runner::state::running_until, R"pbdoc( +Indicates that none of :any:`run` , :any:`run_for` , or :any:`run_until` +has been called since construction or the last call to :any:`init`. +)pbdoc") + .value("timed_out", Runner::state::timed_out, R"pbdoc( +Indicates that none of :any:`run` , :any:`run_for` , or :any:`run_until` +has been called since construction or the last call to :any:`init`. +)pbdoc") + .value("stopped_by_predicate", + Runner::state::stopped_by_predicate, + R"pbdoc( +Indicates that none of :any:`run` , :any:`run_for` , or :any:`run_until` +has been called since construction or the last call to :any:`init`. +)pbdoc") + .value("not_running", Runner::state::not_running, R"pbdoc( +Indicates that none of :any:`run` , :any:`run_for` , or :any:`run_until` +has been called since construction or the last call to :any:`init`. +)pbdoc") + .value("dead", Runner::state::dead, R"pbdoc( +Indicates that none of :any:`run` , :any:`run_for` , or :any:`run_until` +has been called since construction or the last call to :any:`init`. +)pbdoc"); + thing.attr("state") = state; + thing.def("init", + &Runner::init, + R"pbdoc( +Initialize an existing Runner object. +This function puts a :any:`Runner` object back into the same state as if it had been newly default constructed. + +:returns: ``self``. +:rtype: Runner + +.. seealso:: :any:`Runner()` +)pbdoc"); + thing.def("run", + &Runner::run, + R"pbdoc( +Run until finished. Run the main algorithm implemented by a derived +class of :any:`Runner`. +)pbdoc"); + thing.def( + "run_for", + [](Runner& self, std::chrono::nanoseconds t) { + return self.run_for(t); + }, + py::arg("t"), + R"pbdoc( +Run for a specified amount of time. + +For this to work it is necessary to periodically check if :any:`timed_out()` returns ``True``, and to stop if it is, in the :any:`run()` member function of any derived class of :any:`Runner`. + +:param t: the time to run for. +:type t: datetime.timedelta + +.. seealso:: :any:`run_for`)pbdoc"); + thing.def("run_until", + (void(Runner::*)(std::function&)) & Runner::run_until, + py::arg("func"), + R"pbdoc( +Run until a nullary predicate returns true or finished. + +:param func: + a function pointer. + +:type func: + bool(*)() +)pbdoc"); + thing.def("timed_out", + &Runner::timed_out, + R"pbdoc( +Check if the amount of time passed to run_for has elapsed. + +:returns: A ``bool`` +:rtype: bool + +.. seealso:: :any:`run_for`. +)pbdoc"); + thing.def("report_why_we_stopped", + &Runner::report_why_we_stopped, + R"pbdoc( +Report why run stopped. Reports whether :any:`run()` was stopped because +it is :any:`finished()` , :any:`timed_out()` , or :any:`dead()`. +)pbdoc"); + thing.def("finished", + &Runner::finished, + R"pbdoc( +Check if run has been run to completion or not. + +Returns ``True`` if :any:`run()` has been run to completion. For this to work, the implementation of :any:`run()` in a derived class of :any:`Runner` must implement a specialisation of ``finished_impl``. + +:returns: A ``bool``. +:rtype: bool + +.. seealso:: :any:`started()` +)pbdoc"); + thing.def("started", + &Runner::started, + R"pbdoc( +Check if run has been called at least once before. +Returns ``True`` if :any:`run()` has started to run (it can be running or not). + +:returns: A ``bool``. +:rtype: bool + +.. seealso:: :any:`finished()` +)pbdoc"); + thing.def("running", + &Runner::running, + R"pbdoc( +Check if currently running. +Returns ``True`` if :any:`run()` is in the process of running and ``False`` it is not. + +:returns: A ``bool``. +:rtype: bool + +.. seealso:: :any:`finished()` +)pbdoc"); + thing.def("kill", + &Runner::kill, + R"pbdoc( +Stop run from running (thread-safe). +This function can be used to terminate :any:`run()` from another thread. After :any:`kill()` has been called the :any:`Runner` may no longer be in a valid state, but will return ``True`` from :any:`dead()`. + +.. seealso:: :any:`finished()`)pbdoc"); + thing.def("dead", + &Runner::dead, + R"pbdoc( +Check if the runner is dead. +This function can be used to check if we should terminate :any:`run()` because it has been killed by another thread. + +:returns: A ``bool``. +:rtype: bool + +.. seealso:: :any:`kill()` +)pbdoc"); + thing.def("stopped", + &Runner::stopped, + R"pbdoc( +Check if the runner is stopped. This function can be used to check +whether or not :any:`run()` has been stopped for whatever reason. In +other words, it checks if :any:`timed_out()` , :any:`finished()` , or +:any:`dead()`. + +:returns: + A ``bool``. + +:rtype: + bool +)pbdoc"); + thing.def("stopped_by_predicate", + &Runner::stopped_by_predicate, + R"pbdoc( +Check if the runner was stopped, or should stop, because of the argument +last passed to run_until. If ``self`` is running, then the nullary +predicate is called and its return value is returned. If ``self`` is not +running, then ``True`` is returned if and only if the last time ``self`` +was running it was stopped by a call to the nullary predicate passed to +:any:`run_until()`. + +:exceptions: + This function guarantees not to throw a ``LibsemigroupsError``. + +:complexity: + Constant. + +:returns: + A ``bool``. + +:rtype: + bool +)pbdoc"); + thing.def("running_for", + &Runner::running_for, + R"pbdoc( +Check if the runner is currently running for a particular length of +time. If the :any:`Runner` is currently running because its member +function :any:`run_for` has been invoked, then this function returns +``True`` . Otherwise, ``False`` is returned. + +:complexity: + Constant. + +:returns: + A ``bool``. + +:rtype: + bool +)pbdoc"); + thing.def("running_until", + &Runner::running_until, + R"pbdoc( +Check if the runner is currently running until a nullary predicate +returns true. If the :any:`Runner` is currently running because its +member function :any:`run_until` has been invoked, then this function +returns ``True`` . Otherwise, ``False`` is returned. + +:complexity: + Constant. + +:returns: + A ``bool``. + +:rtype: + bool +)pbdoc"); + thing.def("current_state", + &Runner::current_state, + R"pbdoc( +Return the current state. Returns the current state of the :any:`Runner` +as given by :any:`state`. + +:complexity: + Constant. + +:returns: + A value of type ``state``. + +:rtype: + state +)pbdoc"); + } // init_runner + +} // namespace libsemigroups diff --git a/tests/test_runner.py b/tests/test_runner.py new file mode 100644 index 00000000..ba255ad9 --- /dev/null +++ b/tests/test_runner.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, J. D. Mitchell +# +# Distributed under the terms of the GPL license version 3. +# +# The full license is in the file LICENSE, distributed with this software. + +""" +This module contains some tests for the libsemigroups_pybind11 functionality +arising from runner.*pp in libsemigroups. +""" + +# pylint: disable=no-name-in-module, missing-function-docstring, invalid-name + +from datetime import timedelta + +from _libsemigroups_pybind11 import Reporter, delta + + +def test_reporter_000(): + r = Reporter() + assert not r.report() + assert r.report_every() == timedelta(seconds=1) + r.report_every(timedelta(seconds=2)) + assert r.report_every() == timedelta(seconds=2) + r.last_report() + r.reset_last_report() + assert delta(r.last_report()) < timedelta(seconds=1) + r.report_prefix("Banana") + assert r.report_prefix() == "Banana" + r.init() + assert r.report_prefix() == "" + assert r.report_every() == timedelta(seconds=1) + r.report_prefix("Banana") + r.report_every(timedelta(seconds=32)) + + s = Reporter(r) + assert s.report_prefix() == "Banana" + assert s.report_every() == timedelta(seconds=32) + assert s.last_report() == r.last_report() + s.init() + assert s.report_prefix() == "" + assert s.report_every() == timedelta(seconds=1) + + assert s.report_every() == timedelta(seconds=1) From e6e2abfc2848bba480b925b0c158e2b7e09c4684 Mon Sep 17 00:00:00 2001 From: "James D. Mitchell" Date: Mon, 22 Apr 2024 14:28:29 +0100 Subject: [PATCH 2/4] ci: fix typo --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bf72e0bc..f2a0e8c0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -81,7 +81,7 @@ jobs: echo "PKG_CONFIG_PATH=$MAMBA_ROOT_PREFIX/envs/libsemigroups/lib/pkgconfig:$MAMBA_ROOT_PREFIX/envs/libsemigroups/share/pkgconfig:/usr/local/lib/pkgconfig" >> $GITHUB_ENV echo "LD_LIBRARY_PATH=$MAMBA_ROOT_PREFIX/envs/libsemigroups/lib:/usr/local/lib" >> $GITHUB_ENV echo "PATH=$MAMBA_ROOT_PREFIX/envs/libsemigroups/bin:$PATH" >> $GITHUB_ENV - - name: "Install libsemigroups dependices . . ." + - name: "Install libsemigroups dependencies . . ." run : brew install autoconf automake - name: "Install libsemigroups . . ." From 71815b4c102e60149a46005863028b7eeff0c675 Mon Sep 17 00:00:00 2001 From: "James D. Mitchell" Date: Mon, 22 Apr 2024 13:28:38 +0100 Subject: [PATCH 3/4] ci: use ccache to compile libsemigroups --- .github/workflows/tests.yml | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f2a0e8c0..bdb8ada2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,6 +16,9 @@ jobs: defaults: run: shell: bash -l {0} + env: + CXX: "ccache g++" + CXXFLAGS: "-O2 -g" steps: - uses: actions/checkout@v4 - name: "Create micromamba environment" @@ -29,11 +32,17 @@ jobs: run: | echo "PKG_CONFIG_PATH=$MAMBA_ROOT_PREFIX/envs/libsemigroups/lib/pkgconfig:$MAMBA_ROOT_PREFIX/envs/libsemigroups/share/pkgconfig:/usr/local/lib/pkgconfig" >> $GITHUB_ENV echo "LD_LIBRARY_PATH=$MAMBA_ROOT_PREFIX/envs/libsemigroups/lib:/usr/local/lib" >> $GITHUB_ENV + - name: "Setup ccache . . ." + uses: Chocobo1/setup-ccache-action@v1 + with: + update_packager_index: false + install_ccache: true - name: "Install libsemigroups . . ." run: | git clone --depth 1 --branch v3 https://github.com/libsemigroups/libsemigroups.git cd libsemigroups - ./autogen.sh && ./configure CXXFLAGS='-O3 -g' --disable-hpcombi --with-external-fmt && sudo make install -j8 + ./autogen.sh && ./configure CXX="$CXX" CXXFLAGS="$CXXFLAGS" --disable-hpcombi --with-external-fmt && sudo make install -j8 + ccache -s - name: "Python version . . ." run: | python --version @@ -67,6 +76,9 @@ jobs: python_version: ["3.8", "3.9", "3.10", "3.11", "3.12"] runs-on: macOS-latest timeout-minutes: 15 + env: + CXX: "ccache clang++" + CXXFLAGS: "-O2 -g" steps: - uses: actions/checkout@v4 - name: "Create micromamba environment" @@ -84,11 +96,17 @@ jobs: - name: "Install libsemigroups dependencies . . ." run : brew install autoconf automake + - name: "Setup ccache . . ." + uses: Chocobo1/setup-ccache-action@v1 + with: + update_packager_index: false + install_ccache: true - name: "Install libsemigroups . . ." run: | git clone --depth 1 --branch v3 https://github.com/libsemigroups/libsemigroups.git cd libsemigroups - ./autogen.sh && ./configure CXXFLAGS='-O3 -g' --disable-hpcombi --with-external-fmt && sudo make install -j8 + ./autogen.sh && ./configure CXX="$CXX" CXXFLAGS="$CXXFLAGS" --disable-hpcombi --with-external-fmt && sudo make install -j8 + ccache -s - name: "Python version . . ." run: | python --version From a6d2eaaabf9246fa9e9237cf98ac6a991ba329b5 Mon Sep 17 00:00:00 2001 From: "James D. Mitchell" Date: Fri, 12 Apr 2024 11:39:56 +0100 Subject: [PATCH 4/4] Add v3 support for action.*pp --- docs/source/action.rst | 115 +++++++ docs/source/conf.py | 3 + docs/source/index.rst | 3 +- etc/replace-strings-in-doc.py | 2 + libsemigroups_pybind11/__init__.py | 7 +- libsemigroups_pybind11/action.py | 95 ++++++ libsemigroups_pybind11/adapters.py | 65 ++++ src/action.cpp | 424 +++++++++++++++++++++++++ src/adapters.cpp | 68 ++++ src/bmat8.cpp | 477 +++++++++++++++++++++++++++++ src/knuth-bendix.cpp | 2 +- src/main.cpp | 4 + src/main.hpp | 2 + tests/test_action.py | 95 ++++++ tests/test_adapters.py | 39 +++ 15 files changed, 1397 insertions(+), 4 deletions(-) create mode 100644 docs/source/action.rst create mode 100644 libsemigroups_pybind11/action.py create mode 100644 libsemigroups_pybind11/adapters.py create mode 100644 src/action.cpp create mode 100644 src/adapters.cpp create mode 100644 src/bmat8.cpp create mode 100644 tests/test_action.py create mode 100644 tests/test_adapters.py diff --git a/docs/source/action.rst b/docs/source/action.rst new file mode 100644 index 00000000..c6aa5152 --- /dev/null +++ b/docs/source/action.rst @@ -0,0 +1,115 @@ +.. Copyright (c) 2024, James D. Mitchell + + Distributed under the terms of the GPL license version 3. + + The full license is in the file LICENSE, distributed with this software. + +.. currentmodule:: _libsemigroups_pybind11 + +Actions +======= + +This page contains details of the ``RowActionBMat8`` class in +``libsemigroups_pybind11`` for finding actions of semigroups, or groups, on sets. The +notion of an "action" in the context of ``libsemigroups_pybind11`` is analogous to the +notion of an orbit of a group. + +The function :any:`run ` finds points that can be obtained by +acting on the seeds of the action by the generators of the action until no further +points can be found, or :any:`stopped ` returns ``True``. This +is achieved by performing a breadth first search. + +In this documentation we refer to: + +* ``Element`` -- the type of the elements of the underlying semigroup +* ``Point`` -- the type of the objects on which the semigroup elements act +* ``Func`` -- the function that computes the action of ``Element`` on ``Point`` +* ``Side`` -- the side of the action (if it is a left or a right action). + +The following helper functions are also available: + +* :any:`LeftAction` +* :any:`RightAction` + +.. doctest:: + + >>> from libsemigroups_pybind11 import RightAction, BMat8 + >>> o = RightAction(Element=BMat8, Point=BMat8) + >>> o.add_seed( + ... BMat8( + ... [[1, 1, 1, 0], + ... [1, 1, 0, 0], + ... [0, 1, 0, 1], + ... [0, 1, 0, 0]]).row_space_basis() + ... ).add_generator( + ... BMat8([[1, 0, 0, 0], + ... [0, 1, 0, 0], + ... [0, 0, 1, 0], + ... [0, 0, 0, 1]]) + ... ).add_generator( + ... BMat8([[0, 1, 0, 0], + ... [1, 0, 0, 0], + ... [0, 0, 1, 0], + ... [0, 0, 0, 1]]) + ... ).add_generator( + ... BMat8([[0, 1, 0, 0], + ... [0, 0, 1, 0], + ... [0, 0, 0, 1], + ... [1, 0, 0, 0]]) + ... ).add_generator( + ... BMat8([[1, 0, 0, 0], + ... [0, 1, 0, 0], + ... [0, 0, 1, 0], + ... [1, 0, 0, 1]]) + ... ).add_generator( + ... BMat8([[1, 0, 0, 0], + ... [0, 1, 0, 0], + ... [0, 0, 1, 0], + ... [0, 0, 0, 0]]) + ... ) + + >>> o.size() + 553 + +Contents +-------- + +.. autosummary:: + :nosignatures: + + ~RowActionBMat8 + RowActionBMat8.__getitem__ + RowActionBMat8.add_generator + RowActionBMat8.add_seed + RowActionBMat8.cache_scc_multipliers + RowActionBMat8.current_size + RowActionBMat8.empty + RowActionBMat8.init + RowActionBMat8.iterator + RowActionBMat8.multiplier_from_scc_root + RowActionBMat8.multiplier_to_scc_root + RowActionBMat8.position + RowActionBMat8.reserve + RowActionBMat8.root_of_scc + RowActionBMat8.scc + RowActionBMat8.size + RowActionBMat8.word_graph + +Full API +-------- + +.. currentmodule:: libsemigroups_pybind11 + +.. autofunction:: RightAction + +.. autofunction:: LeftAction + +.. currentmodule:: _libsemigroups_pybind11 + +.. autoclass:: RowActionBMat8 + :members: + :show-inheritance: + :class-doc-from: class + + .. autofunction:: libsemigroups_pybind11.action.Action + diff --git a/docs/source/conf.py b/docs/source/conf.py index 72b8dd04..44aaddfb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -68,6 +68,9 @@ # signature if "good type" is a valid (potentially user defined) python type type_replacements = { r"libsemigroups::Presentation, std::allocator > >": r"Presentation", + r"libsemigroups::BMat8": r"BMat8", + r"libsemigroups::WordGraph": r"WordGraph", + r"libsemigroups::Gabow": r"Gabow", } # This dictionary should be of the form class_name -> (pattern, repl), where diff --git a/docs/source/index.rst b/docs/source/index.rst index 91c19d04..dfe4702f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -49,7 +49,8 @@ See the installation instructions: .. toctree:: :caption: API REFERENCE :hidden: - + + action congruences digraph elements diff --git a/etc/replace-strings-in-doc.py b/etc/replace-strings-in-doc.py index e7fc47cb..fd488a3d 100755 --- a/etc/replace-strings-in-doc.py +++ b/etc/replace-strings-in-doc.py @@ -41,6 +41,8 @@ def dive(path): "KnuthBendixRewriteTrie": "KnuthBendix", r"_libsemigroups_pybind11.": "", "libsemigroups::Presentation, std::allocator > >": "Presentation", + "RowActionBMat8": "Action", + "libsemigroups::BMat8": "BMat8", } files = all_html_files(html_path) diff --git a/libsemigroups_pybind11/__init__.py b/libsemigroups_pybind11/__init__.py index 47b15aca..2469afc4 100644 --- a/libsemigroups_pybind11/__init__.py +++ b/libsemigroups_pybind11/__init__.py @@ -51,21 +51,24 @@ to_word, LibsemigroupsError, is_obviously_infinite, + BMat8, + side, ) except ModuleNotFoundError as e: raise ModuleNotFoundError( ( f'{e.msg}, did you forget to run "pip install ." in the libsemigroups_pybind11 ' - f"director? {DISCLAIMER}" + f"directory? {DISCLAIMER}" ) ) from e +from .action import Action, RightAction, LeftAction +from .adapters import ImageRightAction, ImageLeftAction from .knuth_bendix import KnuthBendix from .presentation import Presentation from .transf import PPerm, Transf - # from .froidure_pin import FroidurePin # from .konieczny import Konieczny # from .matrix import Matrix, MatrixKind, make_identity diff --git a/libsemigroups_pybind11/action.py b/libsemigroups_pybind11/action.py new file mode 100644 index 00000000..b92c37d6 --- /dev/null +++ b/libsemigroups_pybind11/action.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, J. D. Mitchell +# +# Distributed under the terms of the GPL license version 3. +# +# The full license is in the file LICENSE, distributed with this software. + +# pylint:disable=no-name-in-module, unused-import + +""" +This package provides the user-facing python part of libsemigroups_pybind11 for +the Action class from libsemigroups. +""" + +from _libsemigroups_pybind11 import RowActionBMat8 as _RowActionBMat8 +from _libsemigroups_pybind11 import ColActionBMat8 as _ColActionBMat8 +from _libsemigroups_pybind11 import BMat8, side +from .adapters import ImageRightAction, ImageLeftAction + + +def Action(**kwargs): # pylint: disable=invalid-name + """ + Construct an :any:`Action` instance. + + :Keyword Arguments: + * *Element* -- the type of the elements in the action + * *Point* -- the type of the points acted on + * *Func* -- the function defining the action + * *Side* -- the side (or handedness) of the action + """ + if len(kwargs) != 4: + raise TypeError(f"expected 4 keyword arguments, found {len(kwargs)}") + for kwarg in ("Element", "Point", "Func", "Side"): + if kwarg not in kwargs: + raise ValueError( + f'unexpected keyword argument "{kwarg}", expected' + + '"Element", "Point", "Func", and "Side"' + ) + + if ( + kwargs["Element"] == BMat8 + and kwargs["Point"] == BMat8 + and kwargs["Func"] == ImageRightAction + and kwargs["Side"] == side.right + ): + return _RowActionBMat8() + if ( + kwargs["Element"] == BMat8 + and kwargs["Point"] == BMat8 + and kwargs["Func"] == ImageLeftAction + and kwargs["Side"] == side.left + ): + return _ColActionBMat8() + raise ValueError("unexpected keyword argument combination") + + +def RightAction( + Func=ImageRightAction, **kwargs +): # pylint: disable=invalid-name + """ + Construct a right :any:`Action` instance. + + :Keyword Arguments: + * *Element* -- the type of the elements in the action + * *Point* -- the type of the points acted on + * *Func* -- the function defining the action (defaults to :any:`ImageRightAction`) + + """ + # TODO probably this will generate unhelpful error messages + return Action( + Func=Func, + Element=kwargs["Element"], + Point=kwargs["Point"], + Side=side.right, + ) + + +def LeftAction(Func=ImageLeftAction, **kwargs): # pylint: disable=invalid-name + """ + Construct a left :any:`Action` instance. + + :Keyword Arguments: + * *Element* -- the type of the elements in the action + * *Point* -- the type of the points acted on + * *Func* -- the function defining the action (defaults to :any:`ImageLeftAction`) + + """ + # TODO probably this will generate unhelpful error messages + return Action( + Func=Func, + Element=kwargs["Element"], + Point=kwargs["Point"], + Side=side.left, + ) diff --git a/libsemigroups_pybind11/adapters.py b/libsemigroups_pybind11/adapters.py new file mode 100644 index 00000000..963ae238 --- /dev/null +++ b/libsemigroups_pybind11/adapters.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, J. D. Mitchell +# +# Distributed under the terms of the GPL license version 3. +# +# The full license is in the file LICENSE, distributed with this software. + +# pylint:disable=no-name-in-module, unused-import + +""" +This package provides the user-facing python part of libsemigroups_pybind11 for +various adapters from libsemigroups. +""" +from _libsemigroups_pybind11 import ( + ImageRightActionBMat8BMat8 as _ImageRightActionBMat8BMat8, + ImageLeftActionBMat8BMat8 as _ImageLeftActionBMat8BMat8, +) +from _libsemigroups_pybind11 import BMat8 + + +def ImageRightAction(**kwargs): # pylint: disable=invalid-name + """ + Construct a ImageRightAction instance. + + :Keyword Arguments: + * *Element* -- the type of the elements in the action + * *Point* -- the type of the points acted on + """ + if len(kwargs) != 2: + raise TypeError(f"expected 2 keyword arguments, found {len(kwargs)}") + for kwarg in ("Element", "Point"): + if kwarg not in kwargs: + raise ValueError( + f'unexpected keyword argument "{kwarg}", expected' + + '"Element" and "Point"' + ) + + if kwargs["Element"] == BMat8 and kwargs["Point"] == BMat8: + return _ImageRightActionBMat8BMat8() + + raise ValueError("unexpected keyword argument combination") + + +def ImageLeftAction(**kwargs): # pylint: disable=invalid-name + """ + Construct a ImageLeftAction instance. + + :Keyword Arguments: + * *Element* -- the type of the elements in the action + * *Point* -- the type of the points acted on + """ + if len(kwargs) != 2: + raise TypeError(f"expected 2 keyword arguments, found {len(kwargs)}") + for kwarg in ("Element", "Point"): + if kwarg not in kwargs: + raise ValueError( + f'unexpected keyword argument "{kwarg}", expected' + + '"Element" and "Point"' + ) + + if kwargs["Element"] == BMat8 and kwargs["Point"] == BMat8: + return _ImageLeftActionBMat8BMat8() + + raise ValueError("unexpected keyword argument combination") diff --git a/src/action.cpp b/src/action.cpp new file mode 100644 index 00000000..9bca8063 --- /dev/null +++ b/src/action.cpp @@ -0,0 +1,424 @@ + +// +// libsemigroups_pybind11 +// Copyright (C) 2024 James D. Mitchell +// +// 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 . +// + +// C++ stl headers.... +#include // for basic_string, string + +// libsemigroups headers +#include +#include +#include + +#include // for format, print + +// pybind11.... +#include +#include + +// libsemigroups_pybind11.... +#include "main.hpp" // for init_TODO + +namespace py = pybind11; + +namespace libsemigroups { + + namespace { + template + void bind_action(py::module& m, std::string const& name) { + using Action_ = Action; + using const_reference_point_type = + typename Action_::const_reference_point_type; + using index_type = typename Action_::index_type; + py::class_ thing(m, name.c_str()); + // The Action_ class doc is in action.rst + + thing.def("__repr__", &detail::to_string); + thing.def(py::init<>(), R"pbdoc( +Default constructor. A constructor that creates an uninitialized +action representing a left or right action. + +:complexity: + Constant. +)pbdoc"); + thing.def(py::init(), R"pbdoc( +Default copy constructor. +)pbdoc"); + thing.def("init", + &Action_::init, + R"pbdoc( +Initialize an existing object. This function puts an action +object back into the same state as if it had been newly default +constructed. + +:returns: ``self``. +:rtype: Action +)pbdoc"); + thing.def("reserve", + &Action_::reserve, + py::arg("val"), + R"pbdoc( +Increase the capacity to a value that is greater or equal to ``val``. + +:param val: new capacity of an action instance. +:type val: int + +:raises ValueError: if ``val`` is too large. + +:complexity: At most linear in the :any:`size()` of the ``Action``. + +:returns: ``self``. +:rtype: Action +)pbdoc"); + thing.def("add_seed", + &Action_::add_seed, + py::arg("seed"), + R"pbdoc( +Add a seed to the action. + +A *seed* is just a starting point for the action, it will belong to the action, as will every point that can be obtained from the seed by acting with the generators of the action. + +:param seed: the seed to add. +:type seed: Point + +:complexity: Constant. + +:returns: ``self`` +:rtype: Action +)pbdoc"); + thing.def("add_generator", + &Action_::add_generator, + py::arg("gen"), + R"pbdoc( +Add a generator to the action. + +An ``Action`` instance represents the action of the semigroup generated by the elements added via this member function. + +:param gen: the generator to add. +:type gen: Element + +:complexity: Constant. + +:returns: ``self``. +:rtype: Action +)pbdoc"); + thing.def("number_of_generators", + &Action_::number_of_generators, + R"pbdoc( +Returns the number of generators. + +:complexity: + Constant. + +:returns: + The number of generators. + +:rtype: + int +)pbdoc"); + thing.def("generators", + &Action_::generators, + R"pbdoc( +Returns the list of generators. + +:complexity: + Constant. + +:returns: + The generators. + +:rtype: + list[Element] +)pbdoc"); + thing.def("position", + &Action_::position, + py::arg("pt"), + R"pbdoc( +Returns the position of a point in the so far discovered points. + +:param pt: the point whose position is sought. +:type pt: Point + +:complexity: Constant. + +:returns: The index of ``pt`` in ``self`` or :any:`UNDEFINED`. +:rtype: int +)pbdoc"); + thing.def("empty", + &Action_::empty, + R"pbdoc( +Checks if the Action contains any points. + +:complexity: + Constant. + +:returns: + ``True`` if the action contains no points (including seeds), and + ``False`` if not. + +:rtype: + bool +)pbdoc"); + thing.def("__getitem__", + &Action_::at, + py::is_operator(), + py::arg("pos"), + R"pbdoc( +Returns the point in a given position. + +:param pos: + the index of an point. + +:type pos: + int + +:raises IndexError: + if ``pos >= current_size())``. + +:complexity: + Constant. + +:returns: + The *Point* in position ``pos`` of the currently enumerated points. + +:rtype: + *Point* +)pbdoc"); + thing.def("size", + &Action_::size, + R"pbdoc( +Returns the size of the fully enumerated action. + +:complexity: + The time complexity is :math:`O(mn)` where :math:`m` is the total + number of points in the orbit and :math:`n` is the number of + generators. + +:returns: + The size of the action, a value of type ``int``. + +:rtype: + int +)pbdoc"); + thing.def("current_size", + &Action_::current_size, + R"pbdoc( +Returns the number of points found so far. + +:complexity: + Constant. + +:returns: + The current size. + +:rtype: + int +)pbdoc"); + thing.def( + "iterator", + [](Action_ const& self) { + return py::make_iterator(self.cbegin(), self.cend()); + }, + R"pbdoc( +Returns an iterator containing the so far enumerated points in the orbit (if any). +No enumeration is triggered by calling this function. + +:complexity: + Constant. + +:returns: + An ``Iterator``. + +:rtype: + Iterator +)pbdoc"); + thing.def( + "cache_scc_multipliers", + [](Action_ const& self) { return self.cache_scc_multipliers(); }, + R"pbdoc( +Returns whether or not we are caching scc multipliers. If the returned +value of this function is ``True`` , then the values returned by +:any:`multiplier_from_scc_root()` and :any:`multiplier_to_scc_root()` +are cached, and not recomputed every time one of these functions is +called. + +:complexity: + Constant. + +:returns: + ``True`` if caching is enabled, and ``False`` if not. + +:rtype: + bool +)pbdoc"); + thing.def( + "cache_scc_multipliers", + [](Action_& self, bool val) { + return self.cache_scc_multipliers(val); + }, + py::arg("val"), + R"pbdoc( +Set whether or not to cache scc multipliers. + +If the parameter **val** is ``True`` , then the values returned by :any:`multiplier_from_scc_root()` and :any:`multiplier_to_scc_root()` are cached, and not recomputed every time one of these functions is called. + +:param val: the value. +:type val: bool + +:complexity: Constant. + +:returns: ``self``. + +:rtype: Action +)pbdoc"); + thing.def("multiplier_from_scc_root", + &Action_::multiplier_from_scc_root, + py::arg("pos"), + R"pbdoc( +Returns a multiplier from a scc root to a given index. + +Returns an element ``x`` of the semigroup generated by the generators in the action such that if ``r`` is the root of the strongly connected component containing ``at(pos)``, then calling ``Func(res, r, x)`` the point ``res`` equals ``at(pos)``. + +:param pos: a position in the action. +:type pos: int + +:complexity: At most :math:`O(mn)` where :math:`m` is the complexity of multiplying elements of type *Element* and :math:`n` is the size of the fully enumerated orbit. + +:raises LibsemigroupsError: if there are no generators yet added or the index ``pos`` is out of range. + +:returns: The multiplier. +:rtype: *Element* +)pbdoc"); + thing.def("multiplier_to_scc_root", + &Action_::multiplier_to_scc_root, + py::arg("pos"), + R"pbdoc( +Returns a multiplier from a given index to a scc root. + +Returns an element ``x`` of the semigroup generated by the generators in the action such that after ``Func(res, at(pos), x)`` the point ``res`` is the root of the strongly connected component containing ``at(pos)``. + +:param pos: a position in the action. +:type pos: int + +:complexity: At most :math:`O(mn)` where :math:`m` is the complexity of multiplying elements of type *Element* and :math:`n` is the size of the fully enumerated orbit. + +:raises LibsemigroupsError: if there are no generators yet added or the index ``pos`` is out of range. + +:returns: The multiplier. +:rtype: *Element* +)pbdoc"); + thing.def( + "root_of_scc", + [](Action_& self, const_reference_point_type x) { + return self.root_of_scc(x); + }, + py::arg("x"), + R"pbdoc( +Returns the root point of a strongly connected component containing an *Point*. + +:param x: the point whose root we want to find. +:type x: Point + +:complexity: At most :math:`O(mn)` where :math:`m` is the complexity of multiplying elements of type *Element* and :math:`n` is the size of the fully enumerated orbit. + +:raises LibsemigroupsError: if the point ``x`` does not belong to the action. + +:returns: The root point. +:rtype: *Point* +)pbdoc"); + thing.def( + "root_of_scc", + [](Action_& self, index_type pos) { return self.root_of_scc(pos); }, + py::arg("pos"), + R"pbdoc( +Returns the root point of a strongly connected component. + +:param pos: the index of the point in the action whose root we want to find. +:type pos: int + +:complexity: At most :math:`O(mn)` where :math:`m` is the complexity of multiplying elements of type *Element* and :math:`n` is the size of the fully enumerated orbit. + +:raises LibsemigroupsError: if the index ``pos`` is out of range. + +:returns: The root point. +:rtype: *Point* +)pbdoc"); + thing.def("word_graph", + &Action_::word_graph, + R"pbdoc( +Returns the word graph of the completely enumerated action. + +:complexity: + At most :math:`O(mn)` where :math:`m` is the complexity of + multiplying elements of type *Element* and :math:`n` is the + size of the fully enumerated orbit. + +:returns: + The word graph of the action. +:rtype: + WordGraph +)pbdoc"); + thing.def("scc", + &Action_::scc, + R"pbdoc( +Returns a Gabow object for strongly connected components. + +:complexity: + At most :math:`O(mn)` where :math:`m` is the complexity of + multiplying elements of type *Element* and :math:`n` is the + size of the fully enumerated orbit. + +:returns: + A :any:`Gabow` object. +:rtype: + Gabow +)pbdoc"); + } // bind_action + } // namespace + void init_action(py::module& m) { + py::enum_(m, "side", R"pbdoc( +This value indicates that the action in an :any:`Action` instance should +be a left action. +)pbdoc") + .value("left", side::left, R"pbdoc( +This value indicates that the action in an :any:`Action` instance should +be a left action. +)pbdoc") + .value("right", side::right, R"pbdoc( +This value indicates that the action in an :any:`Action` instance should +be a right action. +)pbdoc"); + + // One call to bind is required per list of types + bind_action, + ActionTraits, + side::right>(m, "RowActionBMat8"); + bind_action, + ActionTraits, + side::left>(m, "ColActionBMat8"); + } + +} // namespace libsemigroups diff --git a/src/adapters.cpp b/src/adapters.cpp new file mode 100644 index 00000000..303e5716 --- /dev/null +++ b/src/adapters.cpp @@ -0,0 +1,68 @@ +// +// libsemigroups_pybind11 +// Copyright (C) 2024 James D. Mitchell +// +// 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 . +// + +// libsemigroups headers +#include +#include + +// pybind11.... +#include + +// libsemigroups_pybind11.... +#include "main.hpp" // for init_TODO + +namespace py = pybind11; + +namespace libsemigroups { + + namespace { + template + void bind_imagerightaction(py::module& m, std::string const& name) { + using ImageRightAction_ = ImageRightAction; + + py::class_(m, name.c_str()) + .def(py::init<>()) + .def("__call__", + [](ImageRightAction_ const& self, + Point& res, + Point const& pt, + Element const& x) { self(res, pt, x); }); + } // bind_imagerightaction + + template + void bind_imageleftaction(py::module& m, std::string const& name) { + using ImageLeftAction_ = ImageLeftAction; + + py::class_(m, name.c_str()) + .def(py::init<>()) + .def("__call__", + [](ImageLeftAction_ const& self, + Point& res, + Point const& pt, + Element const& x) { self(res, pt, x); }); + } // bind_imageleftaction + + } // namespace + // + void init_imagerightaction(py::module& m) { + // One call to bind is required per list of types + bind_imagerightaction(m, "ImageRightActionBMat8BMat8"); + bind_imageleftaction(m, "ImageLeftActionBMat8BMat8"); + } + +} // namespace libsemigroups diff --git a/src/bmat8.cpp b/src/bmat8.cpp new file mode 100644 index 00000000..1a3cd579 --- /dev/null +++ b/src/bmat8.cpp @@ -0,0 +1,477 @@ +// +// libsemigroups_pybind11 +// Copyright (C) 2024 James D. Mitchell +// +// 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 . +// + +// C std headers.... +// TODO complete or delete + +// C++ stl headers.... +// TODO complete or delete + +// libsemigroups headers +#include + +// pybind11.... +#include +#include +#include + +// libsemigroups_pybind11.... +#include "main.hpp" // for init_bmat8 + +namespace py = pybind11; + +namespace libsemigroups { + + void init_bmat8(py::module& m) { + py::class_ thing(m, + "BMat8", + R"pbdoc( +Defined in ``bmat8.hpp``.Class for fast boolean matrices of dimension up to 8 x 8The member functions for these small matrices over the boolean semiring are more optimised than the generic member functions for boolean matrices. Note that all :any:`BMat8` are represented internally as an 8 x 8 matrix; any entries not defined by the user are taken to be 0. This does not affect the results of any calculations. :any:`BMat8` is a trivial class.)pbdoc"); + thing.def("__repr__", &detail::to_string); + thing.def(py::init<>(), R"pbdoc( +Default constructor.There is no guarantee about the contents of the +matrix constructed. + +:parameters: + (None) + +:exceptions: + This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: + Constant. +)pbdoc"); + thing.def(py::init(), R"pbdoc( + +:param mat: the integer representation of the matrix being constructed. +:type mat: int +Construct from uint64_t.This constructor initializes a :any:`BMat8` to have rows equal to the 8 chunks, of 8 bits each, of the binary representation of ``mat``. + +:exceptions: This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: Constant.)pbdoc"); + thing.def(py::init> const&>(), R"pbdoc( + +:param mat: the vector of vectors representation of the matrix being constructed. +:type mat: std::vector > +A constructor.This constructor initializes a matrix where the rows of the matrix are the vectors in ``mat``. + +:raises LibsemigroupsError: if ``mat`` has 0 rows. + +:raises LibsemigroupsError: if ``mat`` has more than 8 rows. + +:raises LibsemigroupsError: if the rows of ``mat`` are not all of the same length. + +:complexity: Constant.)pbdoc"); + thing.def(py::init(), R"pbdoc( +Default copy constructor. + +:exceptions: + This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: + Constant. +)pbdoc"); + thing.def(py::self == py::self, + py::arg("that"), + R"pbdoc( + +:param that: the BMat8 for comparison. +:type that: BMat8 +Returns ``True`` if ``self`` equals ``that``.This member function checks the mathematical equality of two :any:`BMat8` objects. + +:exceptions: This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: Constant.)pbdoc"); + thing.def(py::self != py::self, + py::arg("that"), + R"pbdoc( + +:param that: the BMat8 for comparison. +:type that: BMat8 +Returns ``True`` if ``self`` does not equal ``that``This member function checks the mathematical inequality of two :any:`BMat8` objects. + +:exceptions: This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: Constant.)pbdoc"); + thing.def(py::self < py::self, + py::arg("that"), + R"pbdoc( + +:param that: the BMat8 for comparison. +:type that: BMat8 +Returns ``True`` if ``self`` is less than ``that``.This member function checks whether a :any:`BMat8` objects is less than another. We order by the results of :any:`to_int()` for each matrix. + +:exceptions: This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: Constant.)pbdoc"); + thing.def(py::self > py::self, + py::arg("that"), + R"pbdoc( + +:param that: the BMat8 for comparison. +:type that: BMat8 +Returns ``True`` if ``self`` is greater than ``that``.This member function checks whether a :any:`BMat8` objects is greater than another. We order by the results of :any:`to_int()` for each matrix. + +:exceptions: This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: Constant.)pbdoc"); + thing.def("get", + &BMat8::get, + py::arg("i"), + py::arg("j"), + R"pbdoc( + +:param i: the row of the entry sought. +:type i: int + +:param j: the column of the entry sought. +:type j: int +Returns the entry in the ( ``i`` , ``j`` )th position.This member function returns the entry in the ( ``i`` , ``j`` )th position. + +:exceptions: This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: Constant. + + +:returns: A ``bool``. + +:rtype: bool +)pbdoc"); + thing.def("set", + &BMat8::set, + py::arg("i"), + py::arg("j"), + py::arg("val"), + R"pbdoc( + +:param i: the row +:type i: int + +:param j: the column +:type j: int + +:param val: the value to set in position (i, j)th +:type val: bool +Sets the ( ``i`` , ``j`` )th position to ``val``.This member function sets the ( ``i`` , ``j`` )th entry of ``self`` to ``val`` . Uses the bit twiddle for setting bits found`here `_. + +:raises LibsemigroupsError: if ``i`` or ``j`` is out of bounds. + +:complexity: Constant. + + +:returns: (None) + +:rtype: None +)pbdoc"); + thing.def("to_int", + &BMat8::to_int, + R"pbdoc( +Returns the integer representation of ``self``.Returns an unsigned +integer obtained by interpreting an 8 x 8 :any:`BMat8` as a sequence of +64 bits (reading rows left to right, from top to bottom) and then +realising this sequence as an unsigned int. + +:exceptions: + This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: + Constant. + +:parameters: + (None) + +:returns: + A ``int``. + +:rtype: + int +)pbdoc"); + thing.def("transpose", + &BMat8::transpose, + R"pbdoc( +Returns the transpose of ``self``.Uses the technique found in`Knu09 +<../biblio.html#knuth2009aa>`_. + +:exceptions: + This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: + Constant. + +:parameters: + (None) + +:returns: + A :any:`BMat8`. + +:rtype: + BMat8 +)pbdoc"); + thing.def(py::self * py::self, + py::arg("that"), + R"pbdoc( + +:param that: the matrix we want to multiply by this. +:type that: BMat8 +Returns the matrix product of ``self`` and ``that``This member function returns the standard matrix product (over the boolean semiring) of two :any:`BMat8` objects. Uses the technique given`here `_. + +:exceptions: This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: Constant. + + +:returns: A :any:`BMat8`. + +:rtype: BMat8 +)pbdoc"); + thing.def("swap", + &BMat8::swap, + py::arg("that"), + R"pbdoc( + +:param that: the BMat8 to swap this with. +:type that: BMat8 +Swaps ``self`` with ``that``.This member function swaps the values of ``self`` and ``that``. + +:exceptions: This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: Constant. + + +:returns: (None) + +:rtype: None +)pbdoc"); + thing.def("row_space_basis", + &BMat8::row_space_basis, + R"pbdoc( +Find a basis for the row space of ``self``.This member function returns +a :any:`BMat8` whose non-zero rows form a basis for the row space of +``self``. + +:exceptions: + This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: + Constant. + +:parameters: + (None) + +:returns: + A :any:`BMat8`. + +:rtype: + BMat8 +)pbdoc"); + thing.def("col_space_basis", + &BMat8::col_space_basis, + R"pbdoc( +Find a basis for the column space of ``self``.This member function +returns a :any:`BMat8` whose non-zero columns form a basis for the +column space of ``self``. + +:exceptions: + This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: + Constant. + +:parameters: + (None) + +:returns: + A :any:`BMat8`. + +:rtype: + BMat8 +)pbdoc"); + thing.def("rows", + &BMat8::rows, + R"pbdoc( +Returns a vector containing the rows of ``self``This member function +returns a :any:`list` of uint8_ts representing the rows of ``self`` . +The vector will always be of length 8, even if ``self`` was constructed +with fewer rows. + +:exceptions: + This function guarantees not to throw a ``LibsemigroupsError``. + +:complexity: + Constant. + +:parameters: + (None) + +:returns: + A std::vector. + +:rtype: + list +)pbdoc"); + thing.def("row_space_size", + &BMat8::row_space_size, + R"pbdoc( +Find the size of the row space of ``self``. + +:exceptions: This function guarantees not to throw a ``LibsemigroupsError``. + +:complexity: :math:`O(n)` where :math:`n` is the return value of this function. + +:parameters: (None) + +.. seealso:: :any:`bmat8::col_space_size`. + +:returns: A ``int``. + +:rtype: int +)pbdoc"); + thing.def("number_of_rows", + &BMat8::number_of_rows, + R"pbdoc( +Returns the number of non-zero rows in ``self``.BMat8s do not know their "dimension" - in effect they are all of dimension 8. However, this member function can be used to obtain the number of non-zero rows of ``self``. + +:exceptions: This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: Constant. + +:parameters: (None) + +.. seealso:: :any:`bmat8::number_of_cols` and :any:`bmat8::minimum_dim`. + +:returns: A ``int``. + +:rtype: int +)pbdoc"); + thing.def("is_regular_element", + &BMat8::is_regular_element, + R"pbdoc( +Check whether ``self`` is a regular element of the full boolean matrix +monoid of appropriate dimension. + +:exceptions: + This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: + Constant. + +:parameters: + (None) + +:returns: + A ``True`` if there exists a boolean matrix ``y`` such that ``x * y * + x = x`` where ``x`` is ``self``. + +:rtype: + bool +)pbdoc"); + thing.def_static( + "random", + [](size_t dim) { return BMat8::random(dim); }, + py::arg("dim") = 8, + R"pbdoc( +Construct a random :any:`BMat8` of dimension at most ``dim``.This static +member function returns a :any:`BMat8` chosen at random, where only the +top-left ``dim`` x ``dim`` entries can be non-zero. + +:exceptions: + This function guarantees not to throw a ``LibsemigroupsError``. + +:parameters: + (None) + +:returns: + A :any:`BMat8`. + +:rtype: + BMat8 +)pbdoc"); + thing.def_static("one", + &BMat8::one, + py::arg("dim"), + R"pbdoc( + +:param dim: the dimension of the identity (default: 8) +:type dim: int +Returns the identity :any:`BMat8`.This member function returns the :any:`BMat8` with the first ``dim`` entries in the main diagonal equal to ``1`` and every other value equal to ``0``. + +:exceptions: This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: Constant. + + +:returns: A :any:`BMat8`. + +:rtype: BMat8 +)pbdoc"); + + m.def("number_of_cols", + &bmat8::number_of_cols, + py::arg("x"), + R"pbdoc( + +:param x: the BMat8 whose number of columns we want. +:type x: BMat8 +Returns the number of non-zero columns in ``x``.BMat8s do not know their "dimension" - in effect they are all of dimension 8. However, this member function can be used to obtain the number of non-zero rows of ``self``. + +:exceptions: This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: Constant. + +.. seealso:: :any:`BMat8::number_of_rows` and :any:`bmat8::minimum_dim`. + + +:returns: A ``int``. + +:rtype: int +)pbdoc"); + m.def("col_space_size", + &bmat8::col_space_size, + py::arg("x"), + R"pbdoc( + +:param x: a BMat8. +:type x: BMat8 +Find the size of the column space of ``x``. + +:exceptions: This function guarantees not to throw a ``LibsemigroupsError``. + +:complexity: :math:`O(n)` where :math:`n` is the return value of this function. + +.. seealso:: :any:`BMat8::row_space_size`. + + +:returns: A ``int``. + +:rtype: int +)pbdoc"); + m.def("minimum_dim", + &bmat8::minimum_dim, + py::arg("x"), + R"pbdoc( + +:param x: a BMat8. +:type x: BMat8 +Find the minimum dimension of ``x``.This member function returns the maximal ``i`` such that row ``i`` or column ``i`` contains a ``1``. + +:exceptions: This function is ``noexcept`` and is guaranteed never to throw. + +:complexity: Constant.)pbdoc"); + } +} // namespace libsemigroups diff --git a/src/knuth-bendix.cpp b/src/knuth-bendix.cpp index 542ed7f7..3cff912d 100644 --- a/src/knuth-bendix.cpp +++ b/src/knuth-bendix.cpp @@ -45,7 +45,7 @@ #include // for PyUnicode_DecodeLatin1 // libsemigroups_pybind11.... -#include "doc-strings.hpp" // for init_knuth_bendix +#include "doc-strings.hpp" // for dead, finished, kill, report #include "main.hpp" // for init_knuth_bendix namespace py = pybind11; diff --git a/src/main.cpp b/src/main.cpp index f9806530..8b2f9f09 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -129,6 +129,10 @@ namespace libsemigroups { py::class_( m, "CongruenceInterface"); + init_imagerightaction(m); + init_action(m); + init_bmat8(m); + init_forest(m); init_gabow(m); init_knuth_bendix(m); diff --git a/src/main.hpp b/src/main.hpp index 832a41a5..0fcedb12 100644 --- a/src/main.hpp +++ b/src/main.hpp @@ -27,6 +27,7 @@ namespace libsemigroups { void init_reporter(py::module&); void init_runner(py::module&); + void init_bmat8(py::module&); void init_imagerightaction(py::module&); void init_forest(py::module&); @@ -38,6 +39,7 @@ namespace libsemigroups { void init_transf(py::module&); void init_words(py::module&); void init_word_graph(py::module&); + void init_action(py::module&); } // namespace libsemigroups diff --git a/tests/test_action.py b/tests/test_action.py new file mode 100644 index 00000000..b794f04c --- /dev/null +++ b/tests/test_action.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, J. D. Mitchell +# +# Distributed under the terms of the GPL license version 3. +# +# The full license is in the file LICENSE, distributed with this software. + +""" +This module contains some tests for the libsemigroups_pybind11 functionality +arising from action.*pp in libsemigroups. +""" + +# pylint: disable=no-name-in-module, missing-function-docstring, invalid-name + + +from libsemigroups_pybind11 import RightAction, LeftAction, BMat8 + + +def test_action_001(): + rows = RightAction(Point=BMat8, Element=BMat8) + rows.add_seed( + BMat8( + [[1, 1, 1, 0], [1, 1, 0, 0], [0, 1, 0, 1], [0, 1, 0, 0]] + ).row_space_basis() + ) + + rows.add_generator( + BMat8([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) + ) + rows.add_generator( + BMat8([[0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) + ) + rows.add_generator( + BMat8([[0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], [1, 0, 0, 0]]) + ) + rows.add_generator( + BMat8([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [1, 0, 0, 1]]) + ) + rows.add_generator( + BMat8([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0]]) + ) + + assert rows.size() == 553 + rows.init() + + rows.add_seed( + BMat8( + [[1, 1, 1, 0], [1, 1, 0, 0], [0, 1, 0, 1], [0, 1, 0, 0]] + ).row_space_basis() + ) + + rows.add_generator( + BMat8([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) + ) + rows.add_generator( + BMat8([[0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) + ) + rows.add_generator( + BMat8([[0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], [1, 0, 0, 0]]) + ) + rows.add_generator( + BMat8([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [1, 0, 0, 1]]) + ) + rows.add_generator( + BMat8([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0]]) + ) + + assert rows.size() == 553 + + cols = LeftAction(Point=BMat8, Element=BMat8) + cols.add_seed( + BMat8( + [[1, 1, 1, 0], [1, 1, 0, 0], [0, 1, 0, 1], [0, 1, 0, 0]] + ).col_space_basis() + ) + + cols.add_generator( + BMat8([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) + ) + cols.add_generator( + BMat8([[0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) + ) + cols.add_generator( + BMat8([[0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], [1, 0, 0, 0]]) + ) + cols.add_generator( + BMat8([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [1, 0, 0, 1]]) + ) + cols.add_generator( + BMat8([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0]]) + ) + + assert cols.size() == 553 + assert cols != rows diff --git a/tests/test_adapters.py b/tests/test_adapters.py new file mode 100644 index 00000000..dce3e5d9 --- /dev/null +++ b/tests/test_adapters.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, J. D. Mitchell +# +# Distributed under the terms of the GPL license version 3. +# +# The full license is in the file LICENSE, distributed with this software. + +""" +This module contains some tests for the libsemigroups_pybind11 functionality +arising from adapters.*pp in libsemigroups. +""" + +# pylint: disable=no-name-in-module, missing-function-docstring, invalid-name + + +from libsemigroups_pybind11 import ImageRightAction, ImageLeftAction, BMat8 + + +def test_adapters_017(): + right = ImageRightAction(Point=BMat8, Element=BMat8) + # Point1, Point2, Element -> Point1 = Point2 ^ Element + A, B, C = BMat8.random(8), BMat8(0), BMat8.one(8) + right(B, A, C) + assert B == A.row_space_basis() + + right(B, C, A) + assert B == A.row_space_basis() + + left = ImageLeftAction(Point=BMat8, Element=BMat8) + left(B, A, C) + assert B == A.col_space_basis() + + left(B, C, A) + assert B == A.col_space_basis() + + right(B, A, A) + left(C, A.transpose(), A.transpose()) + assert B == C.transpose()