Source code for algebraixlib.algebras.multiclans

r"""This module contains the :term:`algebra of multiclans` and related functionality.

A :term:`multiclan` is also a :term:`multiset` (of :term:`relation`\s), and inherits all operations
of the :term:`algebra of multisets`. These are provided in :mod:`~.algebras.multisets`.
"""

# Copyright Algebraix Data Corporation 2015 - 2017
#
# This file is part of algebraixlib <http://github.com/AlgebraixData/algebraixlib>.
#
# algebraixlib is free software: you can redistribute it and/or modify it under the terms of version
# 3 of the GNU Lesser General Public License as published by the Free Software Foundation.
#
# algebraixlib 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along with algebraixlib.
# If not, see <http://www.gnu.org/licenses/>.
# --------------------------------------------------------------------------------------------------
import functools as _functools

import algebraixlib.algebras.multisets as _multisets
import algebraixlib.algebras.relations as _relations
import algebraixlib.algebras.sets as _sets
import algebraixlib.mathobjects as _mo
import algebraixlib.extension as _extension
import algebraixlib.structure as _structure
import algebraixlib.undef as _undef

from ..cache_status import CacheStatus


# --------------------------------------------------------------------------------------------------

[docs]class Algebra: """Provide the operations and relations that are members of the :term:`algebra of multiclans`. This class contains only static member functions. Its main purpose is to provide a namespace for and highlight the operations and relations that belong to the algebra of multiclans. All member functions are also available at the enclosing module scope. """ # ---------------------------------------------------------------------------------------------- # Unary algebra operations. @staticmethod
[docs] def transpose(multiclan: 'P(P(M x M) x N)', _checked=True) -> 'P(P(M x M) x N)': """Return a multiclan where all relations have their left and right components swapped. :return: The :term:`unary multi-extension` of :term:`transposition` from the :term:`algebra of relations` to the :term:`algebra of multiclans`, applied to ``multiclan``, or `Undef()` if ``multiclan`` is not a :term:`multiclan`. """ if _checked: if not is_member(multiclan): return _undef.make_or_raise_undef2(multiclan) else: assert is_member_or_undef(multiclan) if multiclan is _undef.Undef(): return _undef.make_or_raise_undef(2) result = _extension.unary_multi_extend(multiclan, _functools.partial( _relations.transpose, _checked=False), _checked=False) if not result.is_empty: result.cache_multiclan(CacheStatus.IS) result.cache_absolute(multiclan.cached_absolute) result.cache_functional(multiclan.cached_right_functional) result.cache_right_functional(multiclan.cached_functional) result.cache_reflexive(multiclan.cached_reflexive) result.cache_symmetric(multiclan.cached_symmetric) result.cache_transitive(multiclan.cached_transitive) result.cache_regular(multiclan.cached_right_regular) result.cache_right_regular(multiclan.cached_regular) return result
# ---------------------------------------------------------------------------------------------- # Binary algebra operations. @staticmethod
[docs] def compose(multiclan1: 'P(P(M x M) x N)', multiclan2: 'P(P(M x M) x N)', _checked=True) -> 'P(P(M x M) x N)': r"""Return the composition of ``multiclan1`` with ``multiclan2``. :return: The :term:`binary multi-extension` of :term:`composition` from the :term:`algebra of relations` to the :term:`algebra of multiclans`, applied to ``multiclan1`` and ``multiclan2``, or `Undef()` if ``multiclan1`` or ``multiclan2`` are not :term:`multiclan`\s. """ if _checked: if not is_member(multiclan1): return _undef.make_or_raise_undef2(multiclan1) if not is_member(multiclan2): return _undef.make_or_raise_undef2(multiclan2) else: assert is_member_or_undef(multiclan1) assert is_member_or_undef(multiclan2) if multiclan1 is _undef.Undef() or multiclan2 is _undef.Undef(): return _undef.make_or_raise_undef(2) result = _extension.binary_multi_extend(multiclan1, multiclan2, _functools.partial( _relations.compose, _checked=False), _checked=False) if not result.is_empty: result.cache_multiclan(CacheStatus.IS) if multiclan1.cached_is_absolute and multiclan2.cached_is_absolute: result.cache_absolute(CacheStatus.IS) if multiclan1.cached_is_functional and multiclan2.cached_is_functional: result.cache_functional(CacheStatus.IS) if multiclan1.cached_is_right_functional and multiclan2.cached_is_right_functional: result.cache_right_functional(CacheStatus.IS) return result
@staticmethod
[docs] def cross_union(mclan1: 'P(P(M x M) x N)', mclan2: 'P(P(M x M) x N)', _checked=True) -> 'P(P(M x M) x N)': r"""Return the :term:`cross-union` of ``mclan1`` and ``mclan2``. :return: The :term:`binary multi-extension` of :term:`union` from the :term:`algebra of relations` (which inherits it from the :term:`algebra of sets`) to the :term:`algebra of multiclans` applied to ``mclan1`` and ``mclan2``, or `Undef()` if ``mclan1`` or ``mclan2`` are not :term:`multiclan`\s. """ if _checked: if not is_member(mclan1): return _undef.make_or_raise_undef2(mclan1) if not is_member(mclan2): return _undef.make_or_raise_undef2(mclan2) else: assert is_member_or_undef(mclan1) assert is_member_or_undef(mclan2) if mclan1 is _undef.Undef() or mclan2 is _undef.Undef(): return _undef.make_or_raise_undef(2) result = _extension.binary_multi_extend(mclan1, mclan2, _functools.partial( _sets.union, _checked=False), _checked=False) if not result.is_empty: result.cache_multiclan(CacheStatus.IS) if mclan1.cached_is_not_functional or mclan2.cached_is_not_functional: result.cache_functional(CacheStatus.IS_NOT) if mclan1.cached_is_not_right_functional or mclan2.cached_is_not_right_functional: result.cache_right_functional(CacheStatus.IS_NOT) return result
@staticmethod
[docs] def cross_functional_union(mclan1: 'P(P(M x M) x N)', mclan2: 'P(P(M x M) x N)', _checked=True) -> 'P(P(M x M) x N)': r"""Return the :term:`cross-functional union` of ``mclan1`` and ``mclan2``. :return: The :term:`binary multi-extension` of the :term:`functional union` from the :term:`algebra of relations` to the :term:`algebra of multiclans`, applied to ``mclan1`` and ``mclan2``, or `Undef()` if ``mclan1`` or ``mclan2`` are not :term:`multiclan`\s. """ if _checked: if not is_member(mclan1): return _undef.make_or_raise_undef2(mclan1) if not is_member(mclan2): return _undef.make_or_raise_undef2(mclan2) else: assert is_member_or_undef(mclan1) assert is_member_or_undef(mclan2) if mclan1 is _undef.Undef() or mclan2 is _undef.Undef(): return _undef.make_or_raise_undef(2) result = _extension.binary_multi_extend(mclan1, mclan2, _functools.partial( _relations.functional_union, _checked=False), _checked=False) if not result.is_empty: result.cache_multiclan(CacheStatus.IS) result.cache_functional(CacheStatus.IS) if mclan1.cached_is_not_right_functional or mclan2.cached_is_not_right_functional: result.cache_right_functional(CacheStatus.IS_NOT) return result
@staticmethod
[docs] def cross_right_functional_union( multiclan1: 'P(P(M x M) x N)', multiclan2: 'P(P(M x M) x N)', _checked=True) -> 'P(P(M x M) x N)': r"""Return the :term:`cross-right-functional union` of ``multiclan1`` and ``multiclan2``. :return: The :term:`binary multi-extension` of the :term:`right-functional union` from the :term:`algebra of relations` to the :term:`algebra of multiclans`, applied to ``multiclan1`` and ``multiclan2``, or `Undef()` if ``multiclan1`` or ``multiclan2`` are not :term:`multiclan`\s. """ if _checked: if not is_member(multiclan1): return _undef.make_or_raise_undef2(multiclan1) if not is_member(multiclan2): return _undef.make_or_raise_undef2(multiclan2) else: assert is_member_or_undef(multiclan1) assert is_member_or_undef(multiclan2) if multiclan1 is _undef.Undef() or multiclan2 is _undef.Undef(): return _undef.make_or_raise_undef(2) result = _extension.binary_multi_extend(multiclan1, multiclan2, _functools.partial( _relations.right_functional_union, _checked=False), _checked=False) if not result.is_empty: result.cache_multiclan(CacheStatus.IS) result.cache_right_functional(CacheStatus.IS) if multiclan1.cached_is_not_functional or multiclan2.cached_is_not_functional: result.cache_functional(CacheStatus.IS_NOT) return result
@staticmethod
[docs] def cross_intersect(multiclan1: 'P(P(M x M) x N)', multiclan2: 'PP(M x M)', _checked=True) -> 'PP(M x M)': r"""Return the :term:`cross-intersection` of ``multiclan1`` and ``multiclan2``. :return: The :term:`binary multi-extension` of :term:`intersection` from the :term:`algebra of relations` (which inherits it from the :term:`algebra of sets`) to the :term:`algebra of multiclans` applied to ``multiclan1`` and ``multiclan2``, or `Undef()` if ``multiclan1`` or ``multiclan2`` are not :term:`multiclan`\s. """ if _checked: if not is_member(multiclan1): return _undef.make_or_raise_undef2(multiclan1) if not is_member(multiclan2): return _undef.make_or_raise_undef2(multiclan2) else: assert is_member_or_undef(multiclan1) assert is_member_or_undef(multiclan2) if multiclan1 is _undef.Undef() or multiclan2 is _undef.Undef(): return _undef.make_or_raise_undef(2) result = _extension.binary_multi_extend(multiclan1, multiclan2, _functools.partial( _sets.intersect, _checked=False), _checked=False) if not result.is_empty: result.cache_multiclan(CacheStatus.IS) if multiclan1.cached_is_functional or multiclan2.cached_is_functional: result.cache_functional(CacheStatus.IS) if multiclan1.cached_is_right_functional or multiclan2.cached_is_right_functional: result.cache_right_functional(CacheStatus.IS) return result
@staticmethod
[docs] def substrict(multiclan1: 'P(P(M x M) x N)', multiclan2: 'P(P(M x M) x N)', _checked=True) -> 'P(P(M x M) x N)': r"""Return the substriction of ``multiclan1`` and ``multiclan2``. The :term:`substriction` of two :term:`multiclan`\s is a multiclan that contains all :term:`relation`\s from ``multiclan1`` that are a :term:`submultiset` of a relation from ``multiclan2``. :return: The :term:`binary multi-extension` of :term:`substriction` from the :term:`algebra of relations` (which inherits it from the :term:`algebra of sets`) to the :term:`algebra of multiclans` applied to ``multiclan1`` and ``multiclan2``, or `Undef()` if ``multiclan1`` or ``multiclan2`` are not :term:`multiclan`\s. """ if _checked: if not is_member(multiclan1): return _undef.make_or_raise_undef2(multiclan1) if not is_member(multiclan2): return _undef.make_or_raise_undef2(multiclan2) else: assert is_member_or_undef(multiclan1) assert is_member_or_undef(multiclan2) if multiclan1 is _undef.Undef() or multiclan2 is _undef.Undef(): return _undef.make_or_raise_undef(2) result = _extension.binary_multi_extend(multiclan1, multiclan2, _functools.partial( _sets.substrict, _checked=False), _checked=False) for elem, multi in result.data.items(): result.data[elem] = multiclan1.data[elem] if not result.is_empty: result.cache_multiclan(CacheStatus.IS) if multiclan1.cached_is_functional or multiclan2.cached_is_functional: result.cache_functional(CacheStatus.IS) if multiclan1.cached_is_right_functional or multiclan2.cached_is_right_functional: result.cache_right_functional(CacheStatus.IS) if multiclan1.cached_is_reflexive: result.cache_reflexive(CacheStatus.IS) if multiclan1.cached_is_symmetric: result.cache_symmetric(CacheStatus.IS) if multiclan1.cached_is_transitive: result.cache_transitive(CacheStatus.IS) if multiclan1.cached_is_regular: result.cache_regular(CacheStatus.IS) if multiclan1.cached_is_right_regular: result.cache_right_regular(CacheStatus.IS) return result
@staticmethod
[docs] def superstrict(multiclan1: 'P(P(M x M) x N)', multiclan2: 'P(P(M x M) x N)', _checked=True) -> 'P(P(M x M) x N)': r"""Return the superstriction of ``multiclan1`` and ``multiclan2``. The :term:`superstriction` of two :term:`multiclan`\s is a multiclan that contains all :term:`relation`\s from ``multiclan1`` that are a :term:`supermultiset` of a relation from ``multiclan2``. :return: The :term:`binary multi-extension` of :term:`superstriction` from the :term:`algebra of relations` (which inherits it from the :term:`algebra of sets`) to the :term:`algebra of multiclans` applied to ``multiclan1`` and ``multiclan2``, or `Undef()` if ``multiclan1`` or ``multiclan2`` are not :term:`multiclan`\s. """ if _checked: if not is_member(multiclan1): return _undef.make_or_raise_undef2(multiclan1) if not is_member(multiclan2): return _undef.make_or_raise_undef2(multiclan2) else: assert is_member_or_undef(multiclan1) assert is_member_or_undef(multiclan2) if multiclan1 is _undef.Undef() or multiclan2 is _undef.Undef(): return _undef.make_or_raise_undef(2) result = _extension.binary_multi_extend(multiclan1, multiclan2, _functools.partial( _sets.superstrict, _checked=False), _checked=False) for elem, multi in result.data.items(): result.data[elem] = multiclan2.data[elem] if not result.is_empty: result.cache_multiclan(CacheStatus.IS) if multiclan1.cached_is_functional: result.cache_functional(CacheStatus.IS) if multiclan1.cached_is_right_functional: result.cache_right_functional(CacheStatus.IS) if multiclan1.cached_is_reflexive: result.cache_reflexive(CacheStatus.IS) if multiclan1.cached_is_symmetric: result.cache_symmetric(CacheStatus.IS) if multiclan1.cached_is_transitive: result.cache_transitive(CacheStatus.IS) if multiclan1.cached_is_regular: result.cache_regular(CacheStatus.IS) if multiclan1.cached_is_right_regular: result.cache_right_regular(CacheStatus.IS) return result
# For convenience, make the members of class Algebra (they are all static functions) available at # the module level. # pylint: disable=invalid-name #: Convenience redirection to `Algebra.transpose`. transpose = Algebra.transpose #: Convenience redirection to `Algebra.compose`. compose = Algebra.compose #: Convenience redirection to `Algebra.cross_union`. cross_union = Algebra.cross_union #: Convenience redirection to `Algebra.cross_functional_union`. cross_functional_union = Algebra.cross_functional_union #: Convenience redirection to `Algebra.cross_right_functional_union`. cross_right_functional_union = Algebra.cross_right_functional_union #: Convenience redirection to `Algebra.cross_intersect`. cross_intersect = Algebra.cross_intersect #: Convenience redirection to `Algebra.substrict`. substrict = Algebra.substrict #: Convenience redirection to `Algebra.superstrict`. superstrict = Algebra.superstrict # pylint: enable=invalid-name # -------------------------------------------------------------------------------------------------- # Metadata functions.
[docs]def get_name() -> str: """Return the name and :term:`ground set` of this :term:`algebra` in string form.""" return 'Multiclans(M): {ground_set}'.format(ground_set=str(get_ground_set()))
[docs]def get_ground_set() -> _structure.Structure: """Return the :term:`ground set` of this :term:`algebra`.""" return _structure.PowerSet( _structure.CartesianProduct(_relations.get_ground_set(), _structure.GenesisSetN()))
[docs]def get_absolute_ground_set() -> _structure.Structure: """Return the :term:`absolute ground set` of this :term:`algebra`.""" return _structure.PowerSet( _structure.CartesianProduct(_relations.get_absolute_ground_set(), _structure.GenesisSetN()))
[docs]def is_member(obj: _mo.MathObject) -> bool: """Return whether ``obj`` is a member of the :term:`ground set` of this :term:`algebra`. :return: ``True`` if ``obj`` is a :term:`multiclan`, ``False`` if not. .. note:: This function may call :meth:`~.MathObject.get_ground_set` on ``obj``. The result of this operation is cached. """ if obj.cached_multiclan == CacheStatus.UNKNOWN: is_multiclan = obj.get_ground_set().is_subset(get_ground_set()) obj.cache_multiclan(CacheStatus.from_bool(is_multiclan)) return obj.cached_is_multiclan
[docs]def is_member_or_undef(obj: _mo.MathObject) -> bool: """Return whether ``obj`` is either a member of the :term:`ground set` of this :term:`algebra` or :class:`~.Undef`. :return: ``True`` if ``obj`` is either a :term:`relation` or :class:`~.Undef`, ``False`` if not. """ return obj is _undef.Undef() or is_member(obj)
[docs]def is_absolute_member(obj: _mo.MathObject) -> bool: """Return whether ``obj`` is a member of the :term:`absolute ground set` of this algebra. :return: ``True`` if ``obj`` is an :term:`absolute clan`, ``False`` if not. .. note:: This function may call :meth:`~.MathObject.get_ground_set` on ``obj``. The result of this operation is cached. """ if obj.cached_is_not_multiclan: # If known to not be a multiclan, it's also not an absolute multiclan. No further caching. return False # The `or` clause in this `if` statement is a safety thing. It should never hit. if obj.cached_absolute == CacheStatus.UNKNOWN \ or obj.cached_multiclan == CacheStatus.UNKNOWN: # The 'absolute' state has not yet been cached. Determine whether obj is an absolute # multiclan. is_absolute_mclan = obj.get_ground_set().is_subset(get_absolute_ground_set()) if obj.cached_multiclan == CacheStatus.UNKNOWN: if is_absolute_mclan: # If it is an absolute multiclan, it is also a multiclan. obj.cache_multiclan(CacheStatus.IS) else: # If it is not an absolute multiclan, it may still be a multiclan. is_mclan = is_member(obj) if not is_mclan: # If it is neither an absolute multiclan nor a multiclan, exit. (That it is # not a multiclan has already been cached in is_member().) return False # At this point, cached_multiclan == IS. Cache whether this is an absolute multiclan. assert obj.cached_is_multiclan obj.cache_absolute(CacheStatus.from_bool(is_absolute_mclan)) # At this point, cached_multiclan == IS. Return whether it is an absolute multiclan. return obj.cached_is_absolute
# -------------------------------------------------------------------------------------------------- # Related operations, not formally part of the algebra.
[docs]def get_lefts(mclan: 'P(P(M x M) x N)', _checked=True) -> 'P( M )': r"""Return the set of the left components of all couplets in all relations in ``mclan``. :return: The :term:`union` of the :term:`left set`\s of all :term:`relation`\s in ``mclan`` or `Undef()` if ``mclan`` is not a :term:`multiclan`. """ if _checked: if not is_member(mclan): return _undef.make_or_raise_undef2(mclan) else: assert is_member_or_undef(mclan) if mclan is _undef.Undef(): return _undef.make_or_raise_undef(2) if mclan.is_empty: # The left set of an empty set is the empty set return mclan clan_itr = iter(mclan) left_set = _relations.get_lefts(next(clan_itr), _checked=False) for rel in clan_itr: left_set = _sets.union( _relations.get_lefts(rel, _checked=False), left_set, _checked=False) if not left_set.is_empty: if mclan.cached_is_absolute: left_set.cache_absolute(CacheStatus.IS) return left_set
[docs]def get_rights(mclan: 'P(P(M x M) x N)', _checked=True) -> 'P( M )': r"""Return the set of the right components of all couplets in all relations in ``mclan``. :return: The :term:`union` of the :term:`right set`\s of all :term:`relation`\s in ``mclan`` or `Undef()` if ``mclan`` is not a :term:`multiclan`. """ if _checked: if not is_member(mclan): return _undef.make_or_raise_undef2(mclan) else: assert is_member_or_undef(mclan) if mclan is _undef.Undef(): return _undef.make_or_raise_undef(2) if mclan.is_empty: # The right set of an empty set is the empty set return mclan clan_itr = iter(mclan) right_set = _relations.get_rights(next(clan_itr), _checked=False) for rel in clan_itr: right_set = _sets.union( _relations.get_rights(rel, _checked=False), right_set, _checked=False) if not right_set.is_empty: if mclan.cached_is_absolute: right_set.cache_absolute(CacheStatus.IS) return right_set
[docs]def get_rights_for_left(mclan: 'P(P(M x M) x N)', left: '( M )', _checked=True) -> 'P(M x N)': """Return the multiset of the right components of all couplets in the multiclan ``mclan`` associated with the left component ``left``. :return: The :term:`right multiset` of the :term:`multiclan` ``mclan`` associated with the :term:`left component` ``left`` or `Undef()` if ``mclan`` is not a multiclan. """ if _checked: if not is_member(mclan): return _undef.make_or_raise_undef2(mclan) if left is _undef.Undef(): return _mo.Set() left = _mo.auto_convert(left) else: assert is_member_or_undef(mclan) assert _mo.is_mathobject_or_undef(left) if mclan is _undef.Undef(): return _undef.make_or_raise_undef(2) if left is _undef.Undef(): return _mo.Set() clan_itr = iter(mclan) rights = _sets.multify(_relations.get_rights_for_left(next(clan_itr), left, _checked=False)) for rel in clan_itr: rights = _multisets.add( _sets.multify(_relations.get_rights_for_left(rel, left, _checked=False)), rights, _checked=False) if not rights.is_empty: if mclan.cached_is_absolute: rights.cache_absolute(CacheStatus.IS) return rights
[docs]def is_functional(mclan, _checked=True) -> bool: """Return whether ``mclan`` is functional. :return: ``True`` if every :term:`relation` in ``mclan`` is :term:`functional` (is a :term:`function`), ``False`` if not, or `Undef()` if ``mclan`` is not a :term:`multiclan`. """ if _checked: if not is_member(mclan): return _undef.make_or_raise_undef2(mclan) else: assert is_member_or_undef(mclan) if mclan is _undef.Undef(): return _undef.make_or_raise_undef(2) if mclan.cached_functional == CacheStatus.UNKNOWN: # The empty set is already handled in Set().__init__ via flags initialization. functional = all(_relations.is_functional(rel, _checked=False) for rel in mclan.data) mclan.cache_functional(CacheStatus.from_bool(functional)) return mclan.cached_is_functional
[docs]def is_right_functional(mclan, _checked=True) -> bool: """Return whether ``mclan`` is right-functional. :return: ``True`` if every :term:`relation` in ``mclan`` is :term:`right-functional`, ``False`` if not, or `Undef()` if ``mclan`` is not a :term:`multiclan`. """ if _checked: if not is_member(mclan): return _undef.make_or_raise_undef2(mclan) else: assert is_member_or_undef(mclan) if mclan is _undef.Undef(): return _undef.make_or_raise_undef(2) if mclan.cached_right_functional == CacheStatus.UNKNOWN: # The empty set is already handled in Set().__init__ via flags initialization. right_functional = all( _relations.is_right_functional(rel, _checked=False) for rel in mclan.data) mclan.cache_right_functional(CacheStatus.from_bool(right_functional)) return mclan.cached_is_right_functional
[docs]def is_regular(mclan, _checked=True) -> bool: """Return whether ``mclan`` is (left-)regular. :return: ``True`` if ``mclan`` is :term:`regular`, ``False`` if not, or `Undef()` if ``mclan`` is not a :term:`multiclan`. """ if _checked: if not is_member(mclan): return _undef.make_or_raise_undef2(mclan) else: assert is_member_or_undef(mclan) if mclan is _undef.Undef(): return _undef.make_or_raise_undef(2) if mclan.cached_regular == CacheStatus.UNKNOWN: # The empty set is already handled in Set().__init__ via flags initialization. if mclan.cached_is_not_functional: mclan.cache_regular(CacheStatus.IS_NOT) return False itr = iter(mclan.data) rel = next(itr) if not _relations.is_functional(rel): mclan.cache_regular(CacheStatus.IS_NOT) return False left_set = rel.get_left_set() regular = all( _relations.is_functional(rel) and left_set == rel.get_left_set() for rel in itr) mclan.cache_regular(CacheStatus.from_bool(regular)) return mclan.cached_is_regular
[docs]def is_right_regular(mclan, _checked=True) -> bool: """Return whether ``mclan`` is right-regular. :return: ``True`` if ``mclan`` is :term:`right-regular`, ``False`` if not, or `Undef()` if ``mclan`` is not a :term:`multiclan`. """ if _checked: if not is_member(mclan): return _undef.make_or_raise_undef2(mclan) else: assert is_member_or_undef(mclan) if mclan is _undef.Undef(): return _undef.make_or_raise_undef(2) if mclan.cached_right_regular == CacheStatus.UNKNOWN: # The empty set is already handled in Set().__init__ via flags initialization. if mclan.cached_is_not_right_functional: mclan.cache_right_regular(CacheStatus.IS_NOT) return False itr = iter(mclan.data) rel = next(itr) if not _relations.is_right_functional(rel): mclan.cache_right_regular(CacheStatus.IS_NOT) return False right_set = rel.get_right_set() right_regular = all( _relations.is_right_functional(rel) and right_set == rel.get_right_set() for rel in itr) mclan.cache_regular(CacheStatus.from_bool(right_regular)) return mclan.cached_is_regular
[docs]def is_reflexive(mclan, _checked=True) -> bool: """Return whether ``mclan`` is reflexive. :return: ``True`` if ``mclan`` is :term:`reflexive`, ``False`` if it is not, or `Undef()` if ``mclan`` is not a :term:`multiclan`. """ if _checked: if not is_member(mclan): return _undef.make_or_raise_undef2(mclan) else: assert is_member_or_undef(mclan) if mclan is _undef.Undef(): return _undef.make_or_raise_undef(2) if mclan.cached_reflexive == CacheStatus.UNKNOWN: reflexive = all(_relations.is_reflexive(rel, _checked=False) for rel in mclan.data) mclan.cache_reflexive(CacheStatus.from_bool(reflexive)) return mclan.cached_reflexive == CacheStatus.IS
[docs]def is_symmetric(mclan, _checked=True) -> bool: """Return whether ``mclan`` is symmetric. :return: ``True`` if ``mclan`` is :term:`symmetric`, ``False`` if it is not, or `Undef()` if ``mclan`` is not a :term:`multiclan`. """ if _checked: if not is_member(mclan): return _undef.make_or_raise_undef2(mclan) else: assert is_member_or_undef(mclan) if mclan is _undef.Undef(): return _undef.make_or_raise_undef(2) if mclan.cached_symmetric == CacheStatus.UNKNOWN: symmetric = all(_relations.is_symmetric(rel, _checked=False) for rel in mclan.data) mclan.cache_symmetric(CacheStatus.from_bool(symmetric)) return mclan.cached_symmetric == CacheStatus.IS
[docs]def is_transitive(mclan, _checked=True) -> bool: """Return whether ``mclan`` is transitive. :return: ``True`` if ``mclan`` is :term:`transitive`, ``False`` if it is not, or `Undef()` if ``mclan`` is not a :term:`multiclan`. """ if _checked: if not is_member(mclan): return _undef.make_or_raise_undef2(mclan) else: assert is_member_or_undef(mclan) if mclan is _undef.Undef(): return _undef.make_or_raise_undef(2) if mclan.cached_transitive == CacheStatus.UNKNOWN: transitive = all(_relations.is_transitive(rel, _checked=False) for rel in mclan.data) mclan.cache_transitive(CacheStatus.from_bool(transitive)) return mclan.cached_transitive == CacheStatus.IS
[docs]def project(mclan: 'P(P(M x M) x N)', *lefts) -> 'P(P(M x M) x N)': r"""Return a multiclan that contains only the couplets with lefts from ``mclan`` that match ``lefts``. :param mclan: The source data. Must be a :term:`multiclan`. :param lefts: The names of the :term:`left component`\s to match. (If you want to pass in an iterable, you need to prefix it with an asterisk ``*``.) :return: The :term:`projection` of ``mclan`` (a multiclan that contains only :term:`couplet`\s with left components as indicated by ``lefts``), or `Undef()` if ``mclan`` is not a multiclan. """ if not is_member(mclan): return _undef.make_or_raise_undef2(mclan) mclan = compose(mclan, diag(*lefts)) return mclan
def _to_listgen_check_args(mclan: 'P(P(M x M) x N)', offset: '( A )', limit: '( A )', _checked: bool=True) -> (): """Check the arguments of `multiclan_to_listgen` and `order_slice_to_listgen`. Return a tuple with the clean values of ``offset`` and ``limit`` or `Undef()` if there was a problem. """ if _checked: if not is_member(mclan): return _undef.make_or_raise_undef2(mclan) if isinstance(offset, _mo.Atom) and isinstance(offset.value, int): pass elif isinstance(offset, int): offset = _mo.Atom(limit) elif offset is _undef.Undef(): return _undef.make_or_raise_undef(2) else: return _undef.make_or_raise_undef() if isinstance(limit, _mo.Atom) \ and (isinstance(limit.value, int) or limit.value == float('inf')): pass elif isinstance(limit, int) or limit == float('inf'): limit = _mo.Atom(limit) elif limit is _undef.Undef(): return _undef.make_or_raise_undef(2) else: return _undef.make_or_raise_undef() else: if mclan is _undef.Undef() or offset is _undef.Undef() or limit is _undef.Undef(): return _undef.make_or_raise_undef(2) assert is_member(mclan) assert offset.is_atom and isinstance(offset.value, int) assert limit.is_atom and (isinstance(limit.value, int) or limit.value == float('inf')) return offset, limit def _to_listgen_slice(counter_list: [()], offset, limit): """Slice ``counter_list`` according to ``offset`` and ``limit`` considering the multiplicities in the second elements of the 2-tuples in ``counter_list``. Return a list or a list generator. """ start_offset = offset.value end_offset = limit.value + start_offset if start_offset == 0 and end_offset >= float('inf'): # Effectively no offset and limit. Return sorted list as-is. return counter_list # Find the list indices of the first and last tuple (representing a relation and a multiplicity) # and the resulting multiplicities in these first and last tuples (which may be different from # their original multiplicities). start_index = first_multiple = None end_index = last_multiple = None start_offset_next_tuple = 0 for index, counter_entry in enumerate(counter_list): # Calculate the starting offset of the next tuple (by adding the multiplicity of the # current tuple). start_offset_next_tuple += counter_entry[1] if start_index is None and start_offset < start_offset_next_tuple: start_index = index first_multiple = start_offset_next_tuple - start_offset assert first_multiple <= counter_list[start_index][1] if start_index is not None and end_offset <= start_offset_next_tuple: end_index = index last_multiple = counter_list[end_index][1] - (start_offset_next_tuple - end_offset) assert counter_list[end_index][1] > (start_offset_next_tuple - end_offset) break if start_index is None: # No start_index found: start_offset is bigger than all existing offsets. Nothing selected. assert end_index is None return [] if end_index is None: # No end_index found: end_offset is bigger than all existing offsets. Put end_index beyond # list bounds. end_index = len(counter_list) # If needed, adjust the multiplicities of the first and last tuple in the selected range. if start_index < len(counter_list) and counter_list[start_index][1] != first_multiple: counter_list[start_index] = (counter_list[start_index][0], first_multiple) if end_index < len(counter_list) and counter_list[end_index][1] != last_multiple: counter_list[end_index] = (counter_list[end_index][0], last_multiple) # Create and return a generator expression to avoid another copy of the data. limited_list_iter = (counter_tuple for counter_tuple in counter_list[start_index:end_index + 1]) return limited_list_iter
[docs]def multiclan_to_listgen(mclan: 'P(P(M x M) x N)', offset: '( A )', limit: '( A )', _checked: bool=True) -> [()]: r"""Return a generator expression for a list of tuples that contains the relations with indices ``offset <= index < offset + limit``. Note that because of the lack of order the result is not deterministic when using ``offset`` or ``limit``. (See also `order_slice_to_listgen`.) Each tuple contains a relation and its multiplicity. :param mclan: The source data. Must be a :term:`multiclan`. :param offset: An :term:`atom` with an integer value that indicates the index of the first relation (after sorting the multiclan) in the result. Set to ``Atom(0)`` if you want to start with the first relation of the sorted multiclan. :param limit: An atom with an integer value that indicates how many relations should be in the resulting multiclan. When ``limit`` is ``float('inf')``, all relations are returned. """ checked_args = _to_listgen_check_args(mclan, offset, limit, _checked) if checked_args is _undef.Undef(): return _undef.Undef() offset, limit = checked_args if mclan.cardinality == 0: return [] counter_list = list(mclan.data.items()) return _to_listgen_slice(counter_list, offset, limit)
[docs]def order_slice_to_listgen(mclan: 'P(P(M x M) x N)', less_than_f, offset: '( A )', limit: '( A )', _checked: bool=True) -> [()]: r"""Return a generator expression for a list of tuples that contains the relations with indices ``offset <= index < offset + limit``, after having been ordered according to ``order``. Each tuple contains a relation and its multiplicity. :param mclan: The source data. Must be a :term:`multiclan`. :param less_than_f: A function that accepts two :term:`relation`\s as arguments and returns ``True`` if the first one is less than the second one. :param offset: An :term:`atom` with an integer value that indicates the index of the first relation (after sorting the multiclan) in the result. Set to ``Atom(0)`` if you want to start with the first relation of the sorted multiclan. :param limit: An atom with an integer value that indicates how many relations should be in the resulting multiclan. When ``limit`` is ``float('inf')``, all relations are returned. .. note:: Eventually we may add a `MathObject` representation for sequences ('ordered sets'). Until we do, we can't split the application of offset and limit from the ordering/sorting. """ checked_args = _to_listgen_check_args(mclan, offset, limit, _checked) if checked_args is _undef.Undef(): return _undef.Undef() offset, limit = checked_args if mclan.cardinality == 0: return [] counter_list = list(mclan.data.items()) class Key: def __init__(self, relation, less_than): self._rel = relation self._lt = less_than def __lt__(self, other) -> bool: assert isinstance(other, Key) result = self._lt(self._rel, other._rel) return result.value def make_key(multiset_tuple) -> Key: result = Key(relation=multiset_tuple[0], less_than=less_than_f) return result counter_list.sort(key=make_key) return _to_listgen_slice(counter_list, offset, limit)
[docs]def order_slice(mclan: 'P(P(M x M) x N)', less_than_f, offset: '( A )', limit: '( A )', _checked: bool=True) -> 'P(P(M x M) x N)': r"""Return a multiclan that contains the relations with indices ``offset <= index < offset + limit``, after having been ordered according to ``order``. :param mclan: The source data. Must be a :term:`multiclan`. :param less_than_f: A function that accepts two :term:`relation`\s as arguments and returns ``True`` if the first one is less than the second one. :param offset: An :term:`atom` with an integer value that indicates the index of the first relation (after sorting the multiclan) in the result. Set to ``Atom(0)`` if you want to start with the first relation of the sorted multiclan. :param limit: An atom with an integer value that indicates how many relations should be in the resulting multiclan. When ``limit`` is ``float('inf')``, all relations are returned. """ tuple_list_generator = order_slice_to_listgen(mclan, less_than_f, offset, limit, _checked) if tuple_list_generator is _undef.Undef(): return _undef.Undef() mclan = _mo.Multiset({rel: mult for (rel, mult) in tuple_list_generator}, direct_load=True) return mclan
[docs]def from_dict(dict1: dict) -> 'P(P(M x M) x N)': r"""Return a :term:`multiclan` with a single :term:`relation` where the :term:`couplet`\s are the elements of ``dict1``.""" rel = _relations.from_dict(dict1) mclan = _mo.Multiset(rel, direct_load=True) mclan.cache_multiclan(CacheStatus.IS) mclan.cache_functional(CacheStatus.IS) mclan.cache_regular(CacheStatus.IS) return mclan
[docs]def diag(*args, _checked=True) -> 'P(P(M x M) x N)': """Return a multiclan diagonal of the arguments. :param args: Pass in the elements from which the :term:`clan diagonal` is formed. (If you want to pass in an iterable, you need to prefix it with an asterisk ``*``.) """ rels = _relations.diag(*args, _checked=_checked) if rels is _undef.Undef(): return _undef.make_or_raise_undef(2) clan = _mo.Multiset(rels, direct_load=True) clan.cache_multiclan(CacheStatus.IS) clan.cache_functional(CacheStatus.IS).cache_right_functional(CacheStatus.IS) clan.cache_reflexive(CacheStatus.IS).cache_symmetric(CacheStatus.IS) clan.cache_regular(CacheStatus.IS).cache_right_regular(CacheStatus.IS) return clan
[docs]def defined_at(mclan: 'P(P(M x M) x N)', left: '( M )', _checked=True): r"""Return the :term:`relation`\s of ``mclan`` that are defined for ``left``.""" if not is_member(mclan): return _undef.make_or_raise_undef(2) if left is _undef.Undef(): return _undef.make_or_raise_undef(2) result = _extension.unary_multi_extend( mclan, _functools.partial(_relations.defined_at, left=left, _checked=_checked), _checked=_checked) if result is _undef.Undef() or not result: return _undef.make_or_raise_undef2(result) result.cache_multiclan(CacheStatus.IS) if not result.is_empty: if mclan.cached_is_functional: result.cache_functional(CacheStatus.IS) if mclan.cached_is_right_functional: result.cache_right_functional(CacheStatus.IS) if mclan.cached_is_regular: result.cache_regular(CacheStatus.IS) if mclan.cached_is_right_regular: result.cache_right_regular(CacheStatus.IS) return result