#
# 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/>.
"""
A mutable, list-backed variant of :class:`~httk.core.vectors.fracvector.FracVector`.
"""
import operator
from collections.abc import Callable
from math import gcd as calc_gcd
from typing import Any, ClassVar
from httk.core.vectors._nested import (
list_set_slice,
list_slice,
nested_inmap_list,
nested_map_fractions_list,
nested_map_list,
)
from httk.core.vectors.fracvector import FracVector
[docs]
class MutableFracVector(FracVector):
"""
Same as :class:`~httk.core.vectors.fracvector.FracVector`, only this version allows
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 perform 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 has 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
[docs]
nested_map: ClassVar[Callable[..., Any]] = staticmethod(nested_map_list)
[docs]
nested_inmap: ClassVar[Callable[..., Any]] = staticmethod(nested_inmap_list)
[docs]
nested_map_fractions: ClassVar[Callable[..., Any]] = staticmethod(nested_map_fractions_list)
_dup_noms: ClassVar[Callable[..., Any]] = staticmethod(list)
# MutableFracVector's nominators are list-based (and freely reassigned by the set_* methods),
# so the recursive tuple-oriented Noms type from FracVector is loosened to Any here.
def __init__(self, noms: Any, denom: int = 1) -> None:
"""
Low overhead constructor.
Args:
noms: nested *lists* of nominator integers.
denom: the integer denominator.
Represents the tensor ``(1/denom)*(noms)``.
If you want to create a MutableFracVector from something else than lists, use the
:meth:`create` method.
"""
super().__init__(noms, denom)
@classmethod
[docs]
def use(cls, old: Any) -> "FracVector":
"""
Make sure the variable is a MutableFracVector, and if not, convert it.
"""
if isinstance(old, MutableFracVector):
return old
elif isinstance(old, FracVector):
return MutableFracVector.create(old)
try:
return old.to_MutableFracVector()
except Exception:
pass
# Note: the legacy fast path via to_FracVector() was dead code (it always fell through
# to create); this preserves that observable behavior.
try:
old.to_FracVector()
except Exception:
pass
return cls.create(old)
[docs]
def to_FracVector(self) -> FracVector:
"""
Return a FracVector with the values of this MutableFracVector.
"""
return FracVector.create(self.noms, self.denom)
@classmethod
[docs]
def from_FracVector(cls, other: FracVector) -> "MutableFracVector":
"""
Create a MutableFracVector from a FracVector.
"""
return MutableFracVector.create(other.noms, other.denom)
[docs]
def validate(self) -> bool:
# TODO: check all dimensions and make sure noms is a square tensor of only lists
return True
[docs]
def invalidate(self) -> None:
"""
Internal method to call when the MutableFracVector is changed in such a way that cached
properties are invalidated (e.g., ``_dim``).
"""
self._dim = None
#### Python special overloads
def __hash__(self) -> int:
raise Exception("MutableFracVector.__hash__: Cannot (should not) use hash number of a mutable object.")
def __setitem__(self, key: Any, values: Any) -> None:
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: Any) -> "MutableFracVector":
if not isinstance(key, tuple):
key = (key,)
noms = list_slice(self.noms, key)
return MutableFracVector(noms, self.denom)
[docs]
def set_negative(self) -> None:
"""
Change the MutableFracVector inline into its own negative: ``self -> -self``.
"""
nested_inmap_list(operator.neg, self.noms)
[docs]
def set_T(self) -> None:
"""
Change the MutableFracVector inline into its own transpose: ``self -> self.T``.
"""
# TODO: Possible to optimize?
dim = self.dim
noms = self.noms
if len(dim) == 0:
return
elif len(dim) == 1:
self.noms = [
[
noms[col],
]
for col in range(dim[0])
]
return
elif len(dim) == 2:
self.noms = [[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) -> Any:
"""
Change the MutableFracVector inline into its 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("FracVector.inverse: cannot take inverse of singular matrix.")
# The adjugate must be scaled by the ORIGINAL denominator (one factor of self.denom
# falls out of the 1/self.denom**2 in the cofactors, cancelling against the self.denom**3
# in the determinant). The legacy code reassigned self.denom *before* reading it into m,
# so it scaled by the determinant instead -- leaving self a factor det_nom/orig_denom off
# from FracVector.inv(). Capture the original denominator first.
orig_denom = self.denom
if det_nom < 0:
self.denom = -det_nom
m = -orig_denom
else:
self.denom = det_nom
m = orig_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) -> None:
"""
Change the MutableFracVector; reduces any common factor between the 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:
# Integer division keeps the denominator an int, matching FracVector.simplify().
# (The legacy code used true division here, which turned self.denom into a float.)
self.denom = self.denom // gcd
self._inmap_over_noms(lambda x: int(x / gcd))
[docs]
def set_set_denominator(self, resolution: int = 1000000000) -> None:
"""
Change the MutableFracVector; reduces resolution.
Args:
resolution: the new denominator; each element becomes the closest numerical
approximation using this denominator.
"""
denom = self.denom
def limit_resolution_one(x: int) -> int:
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) -> None:
"""
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) -> None:
"""
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: Callable[..., Any], *others: FracVector) -> None:
"""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)