Commit cd583406 authored by Daniel Boeckenhoff's avatar Daniel Boeckenhoff
Browse files

Merge branch 'master' of gitlab.mpcdf.mpg.de:dboe/tfields

parents b460bc66 1eff624f
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# pyc files:
*.pyc
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask instance folder
instance/
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# dotenv
.env
# virtualenv
venv/
ENV/
# Spyder project settings
.spyderproject
# vim files
*.sw*
# folder
tmp/
.idea
from . import core
from . import bases
from . import lib
from .lib import *
# __all__ = ['core', 'points3D']
from .core import Tensors, TensorFields, TensorMaps
# from .points3D import Points3D
from .points3D import Points3D
from .mask import getMask
# methods:
from .mask import getMask # NOQA
from .lib import * # NOQA
# classes:
from .points3D import Points3D # NOQA
from .mesh3D import Mesh3D # NOQA
from .triangles3D import Triangles3D # NOQA
from .scalarField3D import ScalarField3D # NOQA
from .vectorField3D import VectorField3D # NOQA
from .planes3D import Planes3D # NOQA
......@@ -4,8 +4,9 @@ Run doctests on module call
def runDoctests():
import doctest
import pathlib
for f in list(pathlib.Path(__file__).parent.glob('**/*.py')):
doctest.testfile(f.name) # , verbose=True, optionflags=doctest.ELLIPSIS)
parent = pathlib.Path(__file__).parent
for f in list(parent.glob('**/*.py')):
doctest.testfile(str(f.relative_to(parent))) # , verbose=True, optionflags=doctest.ELLIPSIS)
if __name__ == '__main__':
......
......@@ -14,68 +14,17 @@ import sympy.diffgeom
from six import string_types
import warnings
CARTESIAN = 'cartesian'
CYLINDER = 'cylinder'
SPHERICAL = 'spherical'
m = sympy.diffgeom.Manifold('M', 3)
patch = sympy.diffgeom.Patch('P', m)
# cartesian
x, y, z = sympy.symbols('x, y, z')
cartesian = sympy.diffgeom.CoordSystem(CARTESIAN, patch, ['x', 'y', 'z'])
# cylinder
R, Phi, Z = sympy.symbols('R, Phi, Z')
cylinder = sympy.diffgeom.CoordSystem(CYLINDER, patch, ['R', 'Phi', 'Z'])
cylinder.connect_to(cartesian,
[R, Phi, Z],
[R * sympy.cos(Phi),
R * sympy.sin(Phi),
Z],
inverse=False,
fill_in_gaps=False)
cartesian.connect_to(cylinder,
[x, y, z],
[sympy.sqrt(x**2 + y**2),
sympy.Piecewise(
(sympy.pi, ((x < 0) & sympy.Eq(y, 0))),
(0., sympy.Eq(sympy.sqrt(x**2 + y**2), 0)),
(sympy.acos(x / sympy.sqrt(x**2 + y**2)), True)),
z],
inverse=False,
fill_in_gaps=False)
# spherical
r, phi, theta = sympy.symbols('r, phi, theta')
spherical = sympy.diffgeom.CoordSystem(SPHERICAL, patch, ['r', 'phi', 'theta'])
spherical.connect_to(cartesian,
[r, phi, theta],
[r * sympy.cos(phi) * sympy.sin(theta),
r * sympy.sin(phi) * sympy.sin(theta),
r * sympy.cos(theta)],
inverse=False,
fill_in_gaps=False)
cartesian.connect_to(spherical,
[x, y, z],
[sympy.sqrt(x**2 + y**2 + z**2),
sympy.Piecewise(
(0., (sympy.Eq(x, 0) & sympy.Eq(y, 0))),
(sympy.atan(y / x), True)),
sympy.Piecewise(
(0., sympy.Eq(x**2 + y**2 + z**2, 0)),
# (0., (sympy.Eq(x, 0) & sympy.Eq(y, 0) & sympy.Eq(z, 0))),
(sympy.acos(z / sympy.sqrt(x**2 + y**2 + z**2)), True))],
inverse=False,
fill_in_gaps=False)
def getCoordSystem(base):
from . import manifold_3
from .manifold_3 import CARTESIAN, CYLINDER, SPHERICAL
from .manifold_3 import cartesian, cylinder, spherical
def get_coord_system(base):
"""
Args:
base (str or sympy.diffgeom.getCoordSystem)
base (str or sympy.diffgeom.get_coord_system)
Return:
sympy.diffgeom.getCoordSystem
sympy.diffgeom.get_coord_system
"""
if isinstance(base, string_types):
base = getattr(tfields.bases, base)
......@@ -84,10 +33,10 @@ def getCoordSystem(base):
return base
def getCoordSystemName(base):
def get_coord_system_name(base):
"""
Args:
base (str or sympy.diffgeom.getCoordSystem)
base (str or sympy.diffgeom.get_coord_system)
Returns:
str: name of base
"""
......@@ -101,11 +50,11 @@ def getCoordSystemName(base):
return base
def lambdifiedTrafo(baseOld, baseNew):
def lambdifiedTrafo(base_old, base_new):
"""
Args:
baseOld (sympy.CoordSystem)
baseNew (sympy.CoordSystem)
base_old (sympy.CoordSystem)
base_new (sympy.CoordSystem)
Examples:
>>> import numpy as np
......@@ -125,42 +74,73 @@ def lambdifiedTrafo(baseOld, baseNew):
>>> assert new[0, 0] == 5
"""
coords = tuple(baseOld.coord_function(i) for i in range(baseOld.dim))
coords = tuple(base_old.coord_function(i) for i in range(base_old.dim))
f = sympy.lambdify(coords,
baseOld.coord_tuple_transform_to(baseNew,
list(coords)),
base_old.coord_tuple_transform_to(base_new,
list(coords)),
modules='numpy')
return f
def transform(array, baseOld, baseNew):
def transform(array, base_old, base_new):
"""
Transform the input array in place
Args:
array (np.ndarray)
base_old (str or sympy.CoordSystem):
base_new (str or sympy.CoordSystem):
Examples:
Cylindrical coordinates
>>> import tfields
>>> cart = np.array([[0, 0, 0],
... [1, 0, 0],
... [1, 1, 0],
... [0, 1, 0],
... [-1, 1, 0],
... [-1, 0, 0],
... [-1, -1, 0],
... [0, -1, 0],
... [1, -1, 0],
... [0, 0, 1]])
>>> cyl = tfields.bases.transform(cart, 'cartesian', 'cylinder')
>>> cyl
Transform cylinder to spherical. No connection is defined so routing via
cartesian
>>> import numpy as np
>>> import tfields
>>> b = np.array([[5, np.arctan(4. / 3), 0]])
>>> newB = tfields.bases.transform(b, 'cylinder', 'spherical')
>>> newB = b.copy()
>>> tfields.bases.transform(b, 'cylinder', 'spherical')
>>> assert newB[0, 0] == 5
>>> assert round(newB[0, 1], 10) == round(b[0, 1], 10)
"""
baseOld = getCoordSystem(baseOld)
baseNew = getCoordSystem(baseNew)
if baseNew not in baseOld.transforms:
for baseTmp in baseNew.transforms:
if baseTmp in baseOld.transforms:
tmpArray = transform(array, baseOld, baseTmp)
return transform(tmpArray, baseTmp, baseNew)
base_old = get_coord_system(base_old)
base_new = get_coord_system(base_new)
if base_new not in base_old.transforms:
for baseTmp in base_new.transforms:
if baseTmp in base_old.transforms:
transform(array, base_old, baseTmp)
transform(array, baseTmp, base_new)
return
raise ValueError("Trafo not found.")
trafo = tfields.bases.lambdifiedTrafo(baseOld, baseNew)
# very fast trafos in numpy only
shortTrafo = None
try:
shortTrafo = getattr(base_old, 'to_{base_new.name}'.format(**locals()))
except AttributeError:
pass
if shortTrafo:
shortTrafo(array)
return
# trafo via lambdified sympy expressions
trafo = tfields.bases.lambdifiedTrafo(base_old, base_new)
with warnings.catch_warnings():
warnings.filterwarnings('ignore', message="invalid value encountered in double_scalars")
new = np.concatenate([trafo(*coords).T for coords in array])
return new
array[:] = np.concatenate([trafo(*coords).T for coords in array])
if __name__ == '__main__': # pragma: no cover
......
import tfields
import sympy
import numpy as np
CARTESIAN = 'cartesian'
CYLINDER = 'cylinder'
SPHERICAL = 'spherical'
m = sympy.diffgeom.Manifold('M', 3)
patch = sympy.diffgeom.Patch('P', m)
# cartesian
x, y, z = sympy.symbols('x, y, z')
cartesian = sympy.diffgeom.CoordSystem(CARTESIAN, patch, ['x', 'y', 'z'])
# cylinder
R, Phi, Z = sympy.symbols('R, Phi, Z')
cylinder = sympy.diffgeom.CoordSystem(CYLINDER, patch, ['R', 'Phi', 'Z'])
cylinder.connect_to(cartesian,
[R, Phi, Z],
[R * sympy.cos(Phi),
R * sympy.sin(Phi),
Z],
inverse=False,
fill_in_gaps=False)
cartesian.connect_to(cylinder,
[x, y, z],
[sympy.sqrt(x**2 + y**2),
sympy.Piecewise(
(sympy.pi, ((x < 0) & sympy.Eq(y, 0))),
(0., sympy.Eq(sympy.sqrt(x**2 + y**2), 0)),
(sympy.sign(y) * sympy.acos(x / sympy.sqrt(x**2 + y**2)), True)),
z],
inverse=False,
fill_in_gaps=False)
def cylinder_to_cartesian(array):
rPoints = np.copy(array[:, 0])
phiPoints = np.copy(array[:, 1])
array[:, 0] = rPoints * np.cos(phiPoints)
array[:, 1] = rPoints * np.sin(phiPoints)
def cartesian_to_cylinder(array):
x_vals = np.copy(array[:, 0])
y_vals = np.copy(array[:, 1])
problemPhiIndices = np.where((x_vals < 0) & (y_vals == 0))
array[:, 0] = np.sqrt(x_vals**2 + y_vals**2) # r
np.seterr(divide='ignore', invalid='ignore')
# phi
array[:, 1] = np.sign(y_vals) * np.arccos(x_vals / array[:, 0])
np.seterr(divide='warn', invalid='warn')
array[:, 1][problemPhiIndices] = np.pi
tfields.convert_nan(array, 0.) # for cases like cartesian 0, 0, 1
cylinder.to_cartesian = cylinder_to_cartesian
cartesian.to_cylinder = cartesian_to_cylinder
# spherical
r, phi, theta = sympy.symbols('r, phi, theta')
spherical = sympy.diffgeom.CoordSystem(SPHERICAL, patch, ['r', 'phi', 'theta'])
spherical.connect_to(cartesian,
[r, phi, theta],
[r * sympy.cos(phi) * sympy.sin(theta),
r * sympy.sin(phi) * sympy.sin(theta),
r * sympy.cos(theta)],
inverse=False,
fill_in_gaps=False)
cartesian.connect_to(spherical,
[x, y, z],
[sympy.sqrt(x**2 + y**2 + z**2),
sympy.Piecewise(
(0., (sympy.Eq(x, 0) & sympy.Eq(y, 0))),
(sympy.atan(y / x), True)),
sympy.Piecewise(
(0., sympy.Eq(x**2 + y**2 + z**2, 0)),
# (0., (sympy.Eq(x, 0) & sympy.Eq(y, 0) & sympy.Eq(z, 0))),
(sympy.acos(z / sympy.sqrt(x**2 + y**2 + z**2)), True))],
inverse=False,
fill_in_gaps=False)
......@@ -11,6 +11,13 @@ import tfields.bases
import numpy as np
from contextlib import contextmanager
from collections import Counter
import sympy
import scipy as sp
import scipy.spatial # NOQA: F401
import os
from six import string_types
import pathlib
import warnings
np.seterr(all='warn', over='raise')
......@@ -37,7 +44,7 @@ class AbstractNdarray(np.ndarray):
Whene inheriting, three attributes are of interest:
__slots__ (list of str): If you want to add attributes to
your AbstractNdarray subclass, add the attribute name to __slots__
__slotDefaults__ (list): if __slotDefaults__ is None, the
__slot_defaults__ (list): if __slot_defaults__ is None, the
defaults for the attributes in __slots__ will be None
other values will be treaded as defaults to the corresponding
arg at the same position in the __slots__ list.
......@@ -52,9 +59,9 @@ class AbstractNdarray(np.ndarray):
equality check
"""
__slots__ = []
__slotDefaults__ = []
__slot_defaults__ = []
__slotDtypes__ = []
__slotSetters__ = []
__slot_setters__ = []
def __new__(cls, array, **kwargs): # pragma: no cover
raise NotImplementedError("{clsType} type must implement '__new__'"
......@@ -63,24 +70,24 @@ class AbstractNdarray(np.ndarray):
def __array_finalize__(self, obj):
if obj is None:
return
for attr in self._iterSlots():
for attr in self._iter_slots():
setattr(self, attr, getattr(obj, attr, None))
def __array_wrap__(self, out_arr, context=None):
return np.ndarray.__array_wrap__(self, out_arr, context)
@classmethod
def _iterSlots(cls, skipCache=True):
def _iter_slots(cls):
return [att for att in cls.__slots__ if att != '_cache']
@classmethod
def _updateSlotKwargs(cls, kwargs, skipCache=True):
def _update_slot_kwargs(cls, kwargs):
"""
set the defaults in kwargs according to __slotDefaults__
set the defaults in kwargs according to __slot_defaults__
and convert the kwargs according to __slotDtypes__
"""
slotDefaults = cls.__slotDefaults__ + \
[None] * (len(cls.__slots__) - len(cls.__slotDefaults__))
slotDefaults = cls.__slot_defaults__ + \
[None] * (len(cls.__slots__) - len(cls.__slot_defaults__))
slotDtypes = cls.__slotDtypes__ + \
[None] * (len(cls.__slots__) - len(cls.__slotDtypes__))
for attr, default, dtype in zip(cls.__slots__, slotDefaults, slotDtypes):
......@@ -89,13 +96,16 @@ class AbstractNdarray(np.ndarray):
if attr not in kwargs:
kwargs[attr] = default
if dtype is not None:
kwargs[attr] = np.array(kwargs[attr], dtype=dtype)
try:
kwargs[attr] = np.array(kwargs[attr], dtype=dtype)
except Exception as err:
raise ValueError(str(attr) + str(dtype) + str(kwargs[attr]) + str(err))
def __setattr__(self, name, value):
if name in self.__slots__:
index = self.__slots__.index(name)
try:
setter = self.__slotSetters__[index]
setter = self.__slot_setters__[index]
except IndexError:
setter = None
if setter is not None:
......@@ -135,7 +145,7 @@ class AbstractNdarray(np.ndarray):
# Create our own tuple to pass to __setstate__
new_state = pickled_state[2] + tuple([getattr(self, slot) for slot in
self._iterSlots()])
self._iter_slots()])
# Return a tuple that replaces the parent's __setstate__ tuple with our own
return (pickled_state[0], pickled_state[1], new_state)
......@@ -145,13 +155,134 @@ class AbstractNdarray(np.ndarray):
important for unpickling
"""
# Call the parent's __setstate__ with the other tuple elements.
super(AbstractNdarray, self).__setstate__(state[0:-len(self._iterSlots())])
super(AbstractNdarray, self).__setstate__(state[0:-len(self._iter_slots())])
# set the __slot__ attributes
for i, slot in enumerate(reversed(self._iterSlots())):
for i, slot in enumerate(reversed(self._iter_slots())):
index = -(i + 1)
setattr(self, slot, state[index])
def copy(self, *args, **kwargs):
"""
The standard ndarray copy does not copy slots. Correct for this.
Examples:
>>> import tfields
>>> m = tfields.TensorMaps([[1,2,3], [3,3,3], [0,0,0], [5,6,7]],
... maps=[tfields.TensorFields([[0, 1, 2], [1, 2, 3]],
... [1, 2])])
>>> mc = m.copy()
>>> mc is m
False
>>> mc.maps[0].fields[0] is m.maps[0].fields[0]
False
TODO: This function implementation could be more general or maybe redirect to deepcopy?
"""
inst = super(AbstractNdarray, self).copy(*args, **kwargs)
for attr in self._iter_slots():
value = getattr(self, attr)
if hasattr(value, 'copy'):
setattr(inst, attr, value.copy(*args, **kwargs))
elif isinstance(value, list):
list_copy = []
for item in value:
if hasattr(item, 'copy'):
list_copy.append(item.copy(*args, **kwargs))
else:
list_copy.append(item)
setattr(inst, attr, list_copy)
return inst
def save(self, path, *args, **kwargs):
"""
Saving a tensors object by redirecting to the correct save method depending on path
Args:
path (str or buffer)
*args:
forwarded to extension specific method
**kwargs:
extension (str): only needed if path is buffer
... remaining:forwarded to extension specific method
"""
# get the extension
if isinstance(path, string_types):
extension = pathlib.Path(path).suffix.lstrip('.')
# get the save method
try:
save_method = getattr(self,
'_save_{extension}'.format(**locals()))
except:
raise NotImplementedError("Can not find save method for extension: "
"{extension}.".format(**locals()))
# resolve: relative paths, symlinks and ~
path = os.path.realpath(os.path.abspath(os.path.expanduser(path)))
return save_method(path, **kwargs)
@classmethod
def load(cls, path, *args, **kwargs):
"""
load a file as a tensors object.
Args:
path (str or buffer)
*args:
forwarded to extension specific method
**kwargs:
extension (str): only needed if path is buffer
... remaining:forwarded to extension specific method
"""
extension = kwargs.pop('extension', 'npz')
if isinstance(path, string_types):
path = os.path.realpath(os.path.abspath(os.path.expanduser(path)))
extension = pathlib.Path(path).suffix.lstrip('.')
try:
load_method = getattr(cls, '_load_{e}'.format(e=extension))
except:
raise NotImplementedError("Can not find load method for extension: "
"{extension}.".format(**locals()))
return load_method(path, *args, **kwargs)
def _save_npz(self, path, **kwargs):
"""
Args:
path (open file or str/unicode): destination to save file to.
Examples:
>>> import tfields
>>> from tempfile import NamedTemporaryFile
>>> outFile = NamedTemporaryFile(suffix='.npz')
>>> p = tfields.Points3D([[1., 2., 3.], [4., 5., 6.], [1, 2, -6]])
>>> p.save(outFile.name)
>>> _ = outFile.seek(0)
>>> p1 = tfields.Points3D.load(outFile.name)
>>> assert p.equal(p1)
"""
kwargs = {}
for attr in self._iter_slots():
if not hasattr(self, attr):
# attribute in __slots__ not found.
warnings.warn("When saving instance of class {0} Attribute {1} not set."