"""Provide the class :class:`~.Multiset`; it represents a :term:`multiset`."""
# $Id: multiset.py 22702 2015-07-28 20:20:56Z jaustell $
# Copyright Algebraix Data Corporation 2015 - $Date: 2015-07-28 15:20:56 -0500 (Tue, 28 Jul 2015) $
#
# 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 collections as _collections
import types as _types
import algebraixlib.structure as _structure
import algebraixlib.undef as _ud
import algebraixlib.util.miscellaneous as _misc
from algebraixlib.mathobjects.atom import auto_convert
from algebraixlib.mathobjects.couplet import Couplet
from algebraixlib.mathobjects.mathobject import MathObject, raise_if_not_mathobject
# On-demand import 'statements' that avoid problems with circular imports.
def _multisets():
_multisets.algebra = getattr(_multisets, 'algebra', None)
if _multisets.algebra is None:
import algebraixlib.algebras.multisets as multisets
_multisets.algebra = multisets
return _multisets.algebra
# --------------------------------------------------------------------------------------------------
[docs]class Multiset(MathObject):
"""A :term:`multiset` consisting of zero or more different `MathObject` instances."""
def __init__(self, *args, direct_load=False):
"""
:param args: Zero or more unnamed arguments that are placed into the created ``Multiset``.
If you want to pass in an iterable, you need to prefix it with an asterisk ``*``. If
no argument is given or the given iterable is empty, an empty :term:`multiset` is
created. (A Python string of type ``str`` is an iterable, but it is considered a
single, non-iterable argument.) Arguments of type :class:`~collections.Counter` are
loaded directly, and arguments of type `dict` must map values or instances of
`MathObject` to integers; the integers are interpreted as multiplicity values for the
given keys. (In order to create a ``Multiset`` that contains a ``Counter`` or `dict`,
put the ``Counter`` or `dict` in an :class:`~.Atom` or an array first.)
:param direct_load: (Optional) Set to ``True`` if you know that all arguments (or all
elements of the iterable) are instances of `MathObject`.
"""
super().__init__()
elements = args[0] if len(args) == 1 else args
# Normally load an argument. May come from 'elements' or from unnamed arguments.
if isinstance(elements, Multiset):
# A Multiset as argument: create a Multiset that contains a Multiset.
self._data = _collections.Counter({elements: 1})
elif isinstance(elements, _collections.Counter) or isinstance(elements, dict):
self._data = _collections.Counter()
for key in elements.keys():
if direct_load:
self._data[key] = elements[key]
else:
# only asserting in non direct mode, assumption is direct load has good data.
assert isinstance(elements[key], int) and elements[key] > 0
self._data[auto_convert(key)] = elements[key]
elif isinstance(elements, str):
# Strings are iterable, but we treat them as a single value in this case.
self._data = _collections.Counter({auto_convert(elements): 1})
elif isinstance(elements, _collections.Iterable) and not isinstance(elements, MathObject):
# An Iterable (that is not a Multiset, Counter or dict) as argument: create a Multiset
# with all elements.
if direct_load:
self._data = _collections.Counter(elements)
else:
self._data = _collections.Counter([auto_convert(elem) for elem in elements])
else:
# Anything else as argument: create a Multiset with a single element.
if direct_load:
self._data = _collections.Counter({elements: 1})
else:
self._data = _collections.Counter({auto_convert(elements): 1})
self._hash = 0
self._flags._not_relation = True
self._flags._not_clan = True
if self.is_empty:
self._flags._multiclan = True
# ----------------------------------------------------------------------------------------------
# Characteristics of the instance.
@property
def data(self) -> _collections.Counter:
"""Read-only; return the elements of this instance as a :class:`~collections.Counter` of
`MathObject` instances.
"""
return self._data
@property
def cardinality(self) -> int:
"""Read-only; return the number of elements in the :term:`multiset`."""
return sum(self._data.values())
@property
def is_empty(self) -> bool:
"""Return ``True`` if this :term:`multiset` is empty."""
return not self._data
[docs] def has_element(self, elem: MathObject) -> bool:
"""Return whether ``elem`` is an element of this multiset. ``elem`` must be a `MathObject`.
For a more relaxed version (that auto-converts non-`MathObject` arguments into instances of
:class:`~.Atom`) see `__contains__` and the construct ``elem in Multiset``.
"""
raise_if_not_mathobject(elem)
return elem in self.data
[docs] def get_multiplicity(self, elem: MathObject) -> int:
"""Return ``int`` if ``elem`` is an element of this ``Multiset`` where the value is the
number of multiples for ``elem``. ``elem`` must be a `MathObject`.
"""
raise_if_not_mathobject(elem)
return self.data[elem]
[docs] def get_ground_set(self) -> _structure.Structure:
"""Return the :term:`ground set` of the lowest-level algebra of this :class:`Multiset`."""
if len(self.data) == 0:
return _structure.Structure()
elements_ground_set = _structure.Union(elem.get_ground_set() for elem in self.data)
if len(elements_ground_set.data) == 1:
return _structure.PowerSet(_structure.CartesianProduct(
_misc.get_single_iter_elem(elements_ground_set.data), _structure.GenesisSetN()))
else:
return _structure.PowerSet(_structure.CartesianProduct(
elements_ground_set, _structure.GenesisSetN()))
[docs] def is_regular(self) -> bool:
"""Return whether ``self`` is :term:`regular`. Return `Undef()` if not applicable."""
tmp_clan = _multisets().demultify(self)
return tmp_clan.is_regular()
# ----------------------------------------------------------------------------------------------
# (Python-)Special functions.
[docs] def __eq__(self, other):
"""Implement value-based equality. Return ``True`` if type and data match."""
return isinstance(other, Multiset) and (self.data == other.data)
[docs] def __ne__(self, other):
"""Implement value-based inequality. Return ``True`` if type or data don't match."""
return not isinstance(other, Multiset) or (self.data != other.data)
def __lt__(self, other):
"""A value-based comparison for less than. Return ``True`` if ``self < other``."""
return not isinstance(other, Multiset) or (repr(self) < repr(other))
[docs] def __contains__(self, item):
"""Return ``True`` if ``item`` is a member of this multiset. If ``item`` is not a
`MathObject`, it is converted into an :class:`~.Atom`.
This allows Boolean expressions of the form ``element in Multiset``.
"""
return auto_convert(item) in self.data
def __iter__(self):
"""Iterate over the elements of this instance in no particular order. Elements are iterated
over according to their multiplicities."""
for value in sorted(self._data.elements()):
yield value
def __len__(self):
"""Return the cardinality of this multiset, considering multiplicities."""
return sum(self._data.values())
[docs] def __hash__(self):
"""Return a hash based on the value that is calculated on demand and cached."""
if not self._hash:
counter_parts = self.data.items()
multiset_parts = ['algebraixlib.mathobjects.multiset.Multiset']
# noinspection PyTypeChecker
multiset_parts.extend(counter_parts)
self._hash = _misc.get_hash(*multiset_parts)
return self._hash
[docs] def __repr__(self):
"""Return the instance's code representation."""
return 'Multiset({{{0}}})'.format(', '.join(
repr(key) + ': ' + repr(value) for key, value in sorted(self.data.items())))
[docs] def __str__(self):
"""Return the instance's string representation."""
return '[{elems}]'.format(elems=', '.join(
str(key) + ':' + str(value) for key, value in sorted(self._data.items())))
# __getitem__ mechanism for indexing syntax `mo[left]`. ----------------------------------------
def _getitem(self, left):
"""The initial function assigned to ``_getitem_redirect``. Determine whether ``self`` is a
multirelation, set ``_getitem_redirect`` accordingly and return the appropriate
result."""
def is_multirelation():
for elem in self.data.keys():
if not isinstance(elem, Couplet):
return False
return True
# The re-assignment of _getitem_redirect is at instance level; use types.MethodType.
if is_multirelation():
self._getitem_redirect = _types.MethodType(Multiset._getitem_multirelation, self)
else:
self._getitem_redirect = _types.MethodType(Multiset._getitem_undef, self)
return self._getitem_redirect(left)
def _getitem_multirelation(self, left):
"""Return a multiset with the rights of all the couplets that have a left of ``left``."""
left_mo = auto_convert(left)
def _sum_same_left_relations(left_mo_):
return_count = _collections.Counter()
for elem, multi in self.data.items():
if elem.left == left_mo_:
return_count[elem.right] = multi
return return_count
return Multiset(_sum_same_left_relations(left_mo), direct_load=True)
# noinspection PyMethodMayBeStatic,PyUnusedLocal
def _getitem_undef(self, left):
"""Return ``Undef()``. Used for ``self``s that are neither relations nor clans."""
return _ud.Undef()
#: The private member that stores the currently active function. This is used to cache the
#: information whether ``self`` is a multirelation or not.
_getitem_redirect = _getitem
[docs] def __getitem__(self, left):
r"""With the syntax ``mo[left]``, return a set of rights associated with ``left``.
:param left: The :term:`left component` of the :term:`couplet`\(s) of which the
:term:`right component`\(s) are returned.
:return: If ``self`` is a multi-relation, return a :term:`multiset` that contains the
right(s) of the :term:`couplet`\(s) that have a left component that matches ``left``.
(The returned multiset may be empty if no couplet with the given left exists.)
Return `Undef()` if ``self`` is not a multi-relation.
"""
return self._getitem_redirect(left)
# ----------------------------------------------------------------------------------------------
# Overrides for property cache setters that are defined in base class MathObject.
[docs] def cache_is_multiclan(self, value: bool):
"""Cache whether ``self`` is or is not a :term:`multiclan`. See [PropCache]_."""
self._flags.multiclan = value
return self