from __future__ import division
from collections import Set, Hashable, Mapping, MutableMapping
from murasyp.functions import Function
[docs]class Vector(Function, Hashable):
"""Vectors map arguments to zero or a specified rational value
This class derives from :class:`~murasyp.functions.Function`, so its
methods apply here as well.
What has changed:
* This class's members are also hashable, which means they can be used as
keys (in :class:`~collections.Set` and :class:`~collections.Mapping`, and
their built-in variants :class:`set` and :class:`dict`).
>>> {Function({})}
Traceback (most recent call last):
...
TypeError: unhashable type: 'Function'
>>> assert {Vector({})} == {Vector({})}
* Unspecified values are assumed to be zero.
>>> f = Vector({'a': 1.1, 'b': '-1/2','c': 0})
>>> assert f == Vector({'a': '11/10', 'c': 0, 'b': '-1/2'})
>>> f['d']
Fraction(0, 1)
* The union of domains is used under pointwise operations.
>>> f = Vector({'a': 1.1, 'b': '-1/2','c': 0})
>>> g = Vector({'b': '.6', 'c': -2, 'd': 0.0})
>>> assert (
... 1 + (.3 * f - g) / 2 ==
... Vector({'a': '233/200', 'c': 2, 'b': '5/8', 'd': 1})
... )
* A vector's domain can be restricted/extended to a specified
:class:`~collections.Set`.
>>> f = Vector({'a': 1.1, 'b': '-1/2','c': 0})
>>> assert f | {'a','b'} == Vector({'a': '11/10', 'b': '-1/2'})
>>> assert f | {'a','d'} == Vector({'a': '11/10', 'd': 0})
"""
__getitem__ = lambda self, x: (self._mapping[x] if x in self
else self._make_rational(0))
__hash__ = lambda self: hash(tuple(item for item
in self._mapping.items()))
_domain_joiner = lambda self, other: iter(self.domain() | other.domain())
def __or__(self, other):
"""Restriction or extension with zero"""
if isinstance(other, Set):
return type(self)({x: self[x] for x in other})
else:
raise TypeError("the argument must be a Set")
[docs] def mass(self):
"""Sum of the values of the vector
:returns: the sum of all values of the vector
:rtype: :class:`~fractions.Fraction`
>>> Vector({'a': 1, 'b': '-1/2','c': 0}).mass()
Fraction(1, 2)
"""
return sum(self._mapping.values())
[docs] def sum_normalized(self):
"""'Sum-of-values'-normalized version of the vector
:returns: the gamble, but with its values divided by the sum of the
vector's values
:rtype: :class:`~murasyp.vectors.Vector`
>>> assert (
... Vector({'a': 1, 'b': '-1/2','c': 0}).sum_normalized() ==
... Vector({'a': 2, 'c': 0, 'b': -1})
... )
>>> assert Vector({'a': 1, 'b': -1,'c': 0}).sum_normalized() == None
.. note::
``None`` is returned in case the the sum of the vector's values is
zero.
"""
mass = self.mass()
return None if mass == 0 else self / mass
[docs] def is_nonnegative(self):
"""Checks whether all values are nonnegative
:returns: the truth value of the statement
:rtype: :class:`~bool`
>>> Vector({'a': 1.6, 'b': -.6}).is_nonnegative()
False
>>> Vector({'a': .4, 'b': .6}).is_nonnegative()
True
"""
return all(val >= 0 for val in self._mapping.values())
[docs]class Polytope(frozenset):
"""A frozenset of vectors
:type `data`: a non-:class:`~collections.Mapping`
:class:`~collections.Iterable` :class:`~collections.Container` of
arguments accepted by the :class:`~murasyp.vectors.Vector` constructor.
>>> assert (
... Polytope([{'a': 2, 'b': 3}, {'b': 1, 'c': 4}]) ==
... Polytope({Vector({'a': 2, 'b': 3}), Vector({'c': 4, 'b': 1})})
... )
This class derives from :class:`~frozenset`, so its methods apply here as
well.
.. todo::
test all set methods and fix, or elegantly deal with, broken ones
Additional and changed methods:
"""
def __new__(cls, data=[]):
"""Create a polytope"""
if isinstance(data, Mapping):
raise TypeError(str(cls) + " does not accept a mapping,"
+ " but you passed it " + str(data))
else:
return frozenset.__new__(cls, (Vector(element) for element in data))
def __init__(self, data=[]): # only here for Sphinx to pick up the argument
"""Initialize the polytope"""
pass
[docs] def domain(self):
"""The union of the domains of the element vectors
:returns: the union of the domains of the vectors it contains
:rtype: :class:`frozenset`
>>> r = Vector({'a': .03, 'b': -.07})
>>> s = Vector({'a': .07, 'c': -.03})
>>> assert Polytope({r, s}).domain() == frozenset({'a', 'c', 'b'})
"""
return frozenset.union(*(vector.domain() for vector in self))
[docs]class Trafo(MutableMapping):
"""A linear transformation between vector spaces
:type `mapping`: a :class:`~collections.Mapping` (such as a :class:`dict`)
of arguments accepted by the :class:`~murasyp.vectors.Vector`
constructor.
Features:
* The transformation can be applied to :class:`~murasyp.vectors.Vector`
(and :class:`~collections.Set` thereof) using the ``<<`` operator:
>>> T = Trafo()
>>> T['a'] = {('a', 'c'): 1, ('a', 'd'): 1}
>>> T['b'] = {('b', 'c'): 1, ('b', 'd'): 1}
>>> assert (
... T << Vector({'a': 1, 'b': 2}) ==
... Vector({('b', 'c'): 2, ('a', 'd'): 1,
... ('a', 'c'): 1, ('b', 'd'): 2})
... )
>>> P = Polytope({Vector({'a': -2})})
>>> assert T << P == Polytope({Vector({('a', 'd'): -2, ('a', 'c'): -2})})
"""
def __init__(self, mapping={}):
"""Create a transformation"""
if isinstance(mapping, Mapping):
self._mapping = {arg: Vector(value)
for arg, value in mapping.items()}
else:
raise TypeError("specify a mapping")
__len__ = lambda self: len(self._mapping)
__iter__ = lambda self: iter(self._mapping)
__contains__ = lambda self, element: element in self._mapping
__getitem__ = lambda self, element: self._mapping[element]
def __setitem__(self, element, value):
self._mapping.__setitem__(element, Vector(value))
def __delitem__(self, element):
mapping = self._mapping
del mapping[element]
def __lshift__(self, other):
"""Applying the transformation"""
if isinstance(other, Vector):
return sum(value * self[arg] for arg, value in other.items())
if isinstance(other, Set):
return type(other)(self << x for x in other)
else:
raise TypeError("the argument must be a Vector or a (nested) Set thereof")