#
# The high-throughput toolkit (httk)
# Copyright (C) 2012-2015 Rickard Armiento
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import fractions
import operator
import itertools
from httk.core.vectors.fracvector import FracVector, nested_map_list, nested_map_fractions_list, nested_reduce_fractions, nested_reduce
from httk.core.vectors.vector import MutableVector, integer_types
try:
from math import gcd as calc_gcd
except ImportError:
from fractions import gcd as calc_gcd
# Utility functions needed before defining the class (due to them being statically assigned)
[docs]def nested_inmap_list(op, *ls):
"""
Like inmap, but work for nested lists
"""
if isinstance(ls[0], (list, tuple)):
if len(ls[0]) == 0 or not isinstance(ls[0][0], list):
inmap(op, *ls)
return
inmap(lambda *items: nested_map_list(op, *items), *ls)
return
raise Exception("nested_inmap_list: called with non-list, not possible to do inmap replacement on scalars:"+str(op)+":"+str(ls))
# Class definition
[docs]class MutableFracVector(FracVector, MutableVector):
"""
Same as FracVector, only, this version allow assignment of elements, e.g., ::
mfracvec[2,7] = 5
and, e.g., ::
mfracvec[:,7] = [1,2,3,4]
Other than this, the FracVector methods exist and do the same, i.e., they return *copies* of the fracvector, rather
than modifying it.
However, methods have also been added named with set_* prefixes which performs mutating operations, e.g., ::
A.set_T()
replaces A with its own transpose, whereas ::
A.T()
just returns a new MutableFracVector that is the transpose of A, leaving A unmodified.
"""
# Implementation note: REMEMBER TO CALL 'self._invalidate()' if any mutation have been done on the frac vector that may have broken
# cached properties, e.g., self._dim
# Overloaded methods now replaced to be list-based rather than tuple-based
nested_map = staticmethod(nested_map_list)
nested_inmap = staticmethod(nested_inmap_list)
nested_map_fractions = staticmethod(nested_map_fractions_list)
_dup_noms = staticmethod(list)
def __init__(self, noms, denom):
"""
Low overhead constructor.
noms: nested *tuples* (may not be a lists!) of nominator integers
denom: integer denominator
Represents the tensor (1/denom)*(noms)
If you want to create a MutableFracVector from something else than tuples, use the MutableFracVector.create() method.
"""
super(MutableFracVector, self).__init__(noms, denom)
[docs] @classmethod
def use(cls, old):
"""
Make sure variable is a MutableFracVector, and if not, convert it.
"""
if isinstance(old, MutableFracVector):
return old
elif isinstance(old, FracVector):
return MutableFracVector.create(old)
else:
try:
return old.to_MutableFracVector()
except Exception:
pass
try:
fracvec = old.to_FracVector()
return cls.__init__(fracvec.noms, fracvec.denom)
except Exception:
pass
return cls.create(old)
[docs] def to_FracVector(self):
"""
Return a FracVector with the values of this MutableFracVector.
"""
return FracVector.create(self.noms, self.denom)
[docs] @classmethod
def from_FracVector(cls, other):
"""
Create a MutableFracVector from a FracVector.
"""
return MutableFracVector.create(other.noms, other.denom)
[docs] def validate(self):
# TODO: check all dimensions and make sure noms is a square tensor of only lists
return True
[docs] def invalidate(self):
"""
Internal method to call when MutableFracVector is changed in such a way that cached properties
are invalidated (e.g., _dim)
"""
self._dim = None
#### Python special overloads
def __hash__(self):
raise Exception("MutableFracVector.__hash__: Cannot (should not) use hash number of a mutable object.")
def __setitem__(self, key, values):
if not isinstance(key, tuple):
key = (key,)
other = FracVector.create(values)
one, two, denom = self.set_common_denom(self, other)
self.noms = one.noms
self.denom = denom
list_set_slice(self.noms, key, two.noms)
if not self.validate():
raise Exception("MutableFracVector: assignment slice: "+str(key)+" mismatch with vector dimensions: "+str(self.dim))
def __getitem__(self, key):
if not isinstance(key, tuple):
key = (key,)
noms = list_slice(self.noms, key)
return MutableFracVector(noms, self.denom)
[docs] def set_negative(self):
"""
Changes MutableFracVector inline into own negative: self -> -self
"""
nested_inmap_list(operator.neg, self.noms)
[docs] def set_T(self):
"""
Changes MutableFracVector inline into own transpose: self -> self.T
"""
#TODO: Possible to optimize?
dim = self.dim
if len(dim) == 0:
return
elif len(dim) == 1:
self.noms = [[self.noms[col], ] for col in range(dim[0])]
return
elif len(dim) == 2:
self.noms = [[self.noms[col][row] for col in range(dim[0])] for row in range(dim[1])]
return
raise Exception("FracVector.T(): on non 1 or 2 dimensional object not implemented")
[docs] def set_inv(self):
"""
Changes MutableFracVector inline into own inverse: self -> self^-1
"""
dim = self.dim
if dim == ():
return self.__class__(self.denom, self.nom)
if dim != (3, 3):
raise Exception("FracVector.inv: only scalar and 3x3 matrix implemented")
# We are dividing with a determinant giving self.denom**3 in nominator, and
# from the matrix 1/self.denom**2 falls out => one factor of self.denom in nominator
det = self.det()
det_nom = det.nom
if det_nom == 0:
raise Exception("ExactVector.inverse: cannot take inverse of singular matrix.")
if det_nom < 0:
self.denom = -det_nom
m = -self.denom
else:
self.denom = det_nom
m = self.denom
A = self.noms
self.noms = [[m * (A[1][1] * A[2][2] - A[1][2] * A[2][1]), m * (A[0][2] * A[2][1] - A[0][1] * A[2][2]), m * (A[0][1] * A[1][2] - A[0][2] * A[1][1])],
[m * (A[1][2] * A[2][0] - A[1][0] * A[2][2]), m * (A[0][0] * A[2][2] - A[0][2] * A[2][0]), m * (A[0][2] * A[1][0] - A[0][0] * A[1][2])],
[m * (A[1][0] * A[2][1] - A[1][1] * A[2][0]), m * (A[0][1] * A[2][0] - A[0][0] * A[2][1]), m * (A[0][0] * A[1][1] - A[0][1] * A[1][0])]]
[docs] def set_simplify(self):
"""
Changes MutableFracVector; reduces any common factor between denominator and all nominators
"""
if self.denom != 1:
gcd = self._reduce_over_noms(lambda x, y: calc_gcd(x, abs(y)), initializer=self.denom)
if gcd != 1:
self.denom = self.denom / gcd
self._inmap_over_noms(lambda x: int(x / gcd))
[docs] def set_set_denominator(self, resolution=1000000000):
"""
Changes MutableFracVector; reduces resolution.
resolution is the new denominator, each element becomes the closest numerical approximation using this denominator.
"""
denom = self.denom
def limit_resolution_one(x):
low = (x * resolution) // denom
if x * resolution * 2 > (low * 2 + 1) * denom:
return low + 1
else:
return low
self._inmap_over_noms(limit_resolution_one)
self.denom = resolution
[docs] def set_normalize(self):
"""
Add/remove an integer +/-N to each element to place it in the range [0,1)
"""
self._inmap_over_noms(lambda x: x - self.denom * (x // self.denom))
[docs] def set_normalize_half(self):
"""
Add/remove an integer +/-N to each element to place it in the range [-1/2,1/2)
This is useful to find the shortest vector C between two points A, B in a space with periodic boundary conditions [0,1):
C = (A-B).normalize_half()
"""
self._inmap_over_noms(lambda x: 2 * x - (2 * self.denom) * ((((2 * x) // self.denom) + 1) // 2))
self.denom = 2*self.denom
#### Private methods
def _inmap_over_noms(self, op, *others):
"""Inmap an operation over all nominators"""
othernoms = [x.noms for x in others]
if isinstance(self.noms, (tuple, list)):
self.nested_inmap(op, self.noms, *othernoms)
else:
self.noms = op(self.noms, *othernoms)
[docs]def list_slice(l, key):
"""
Given a python slice (i.e., what you get to __getitem__ when you write A[3:2]), cut out the relevant
nested list.
"""
if isinstance(key[0], (integer_types + (slice,))):
slicedlist = l[key[0]]
else:
slicedlist = tuple([l[i] for i in key[0]])
cdr = key[1:]
if len(cdr) > 0:
if isinstance(key[0], slice):
return tuple(list_slice(slicedlist[i], cdr) for i in range(len(slicedlist)))
else:
return list_slice(slicedlist, cdr)
return slicedlist
[docs]def list_set_slice(l, key, values):
"""
Given:
l = list,
key = python slice (i.e., what you get to __setitem__ when you write A[3:2]=[2,5])
values = a list of values,
change the elements specified by the slice in key to those given by values.
"""
if len(key) == 1:
if isinstance(key[0], (slice,)):
l[key[0]] = list(values)
else:
l[key[0]] = values
return
if isinstance(key[0], integer_types):
list_set_slice(l[key[0]], key[1:], values)
elif isinstance(key[0], slice):
idxs = key[0].indices(len(l))
i = 0
for idx in range(*idxs):
list_set_slice(l[idx], key[1:], values[i])
i += 1
else:
for idx in key[0]:
list_set_slice(l[idx], key[1:], values[i])
i += 1
[docs]def inmap(f, x):
"""
Like built-in map, but work on a list and *replace* the elements in the list with the result of the mapping.
"""
for i, v in enumerate(x):
x[i] = f(v)
[docs]def main():
a = FracVector.create([[2, 3, 5], [3, 5, 4], [4, 6, 7]])
b = MutableFracVector.from_FracVector(a)
print(b)
print(b.T())
print(b.inv())
b[2, 1:] = [4711, 23]
print(b)
b.set_negative()
print(b)
if __name__ == "__main__":
main()