diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..5c624b4b86fcdb92f8b04bd56ab533f6996d4191 --- /dev/null +++ b/.gitignore @@ -0,0 +1,95 @@ +# 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 diff --git a/tfields/mesh3D.py b/tfields/mesh3D.py new file mode 100644 index 0000000000000000000000000000000000000000..39112538c1e53ee2a64200075b3cc341367ba03b --- /dev/null +++ b/tfields/mesh3D.py @@ -0,0 +1,1708 @@ +#!/usr/bin/env +# encoding: utf-8 +""" +Author: Daniel Boeckenhoff +Mail: daniel.boeckenhoff@ipp.mpg.de + +part of tfields library +""" +import numpy as np +import os +import sympy +import warnings +import tfields +import ioTools +import mplTools +import decoTools +import pyTools +import symTools +from sympy.abc import y, z +from scipy.spatial import ConvexHull +import matplotlib.pyplot as plt +import matplotlib.colors as colors +import loggingTools +import cuttingTree + +logger = loggingTools.Logger(__name__) + + +class Mesh3D(tfields.TensorMaps): + # pylint: disable=R0904 + """ + Points3D child used as vertices combined with faces to build a geometrical mesh of triangles + Examples: + >>> m = tfields.Mesh3D([[1,2,3], [3,3,3], [0,0,0], [5,6,7]], faces=[[0, 1, 2], [1, 2, 3]]) + >>> m + Mesh3D([[ 1., 2., 3.], + [ 3., 3., 3.], + [ 0., 0., 0.], + [ 5., 6., 7.]]) + >>> m.faces + array([[0, 1, 2], + [1, 2, 3]]) + + conversion to points only + >>> tfields.Points3D(m) + Points3D([[ 1., 2., 3.], + [ 3., 3., 3.], + [ 0., 0., 0.], + [ 5., 6., 7.]]) + + Empty instances + >>> m = tfields.Mesh3D([]); + + going from Mesh3D to Triangles3D instance is easy and will be cached. + >>> m = tfields.Mesh3D([[1,0,0], [0,1,0], [0,0,0]], faces=[[0, 1, 2]]); + >>> m.triangles + Triangles3D([[ 1., 0., 0.], + [ 0., 1., 0.], + [ 0., 0., 0.]]) + + a list of scalars is assigned to each face + >>> m.faceScalars + array([], shape=(1, 0), dtype=float64) + >>> mScalar = tfields.Mesh3D([[1,0,0], [0,1,0], [0,0,0]], [[0, 1, 2]], faceScalars=[.5]); + >>> mScalar.faceScalars + array([[ 0.5]]) + + adding together two meshes: + >>> m2 = tfields.Mesh3D([[1,0,0],[2,0,0],[0,3,0]], [[0,1,2]], faceScalars=[.7]) + >>> msum = tfields.Mesh3D([mScalar, m2]) + >>> msum + Mesh3D([[ 1., 0., 0.], + [ 0., 1., 0.], + [ 0., 0., 0.], + [ 1., 0., 0.], + [ 2., 0., 0.], + [ 0., 3., 0.]]) + >>> msum.faces + array([[0, 1, 2], + [3, 4, 5]]) + + Saving and reading + >>> from tempfile import NamedTemporaryFile + >>> outFile = NamedTemporaryFile(suffix='.npz') + >>> m.savez(outFile.name) + >>> _ = outFile.seek(0) + >>> m1 = tfields.Mesh3D.createFromFile(outFile.name) + >>> bool(np.all(m == m1)) + True + >>> m1.faces + array([[0, 1, 2]]) + + """ + + __slots__ = ['faces', 'faceScalars', 'coordSys', '_cache'] + + def __new__(cls, tensors, **kwargs): + if not issubclass(type(tensors), Mesh3D): + kwargs['dim'] = 3 + return super(Mesh3D, cls).__new__(cls, tensors, **kwargs) + + @classmethod + def _updateSlotKwargs(cls, kwargs, skipCache=True): + faces = kwargs.pop('faces', None) + faceScalars = kwargs.pop('faceScalars', None) + + # Add faces + if faces is None or len(faces) == 0: + faces = np.empty((0, 3), dtype=int) + faces = np.array(faces, dtype=int) + kwargs['faces'] = faces + + # Add faceScalars + if faceScalars is None or len(faceScalars) == 0: + faceScalars = np.empty((len(faces), 0), dtype=float) + if not len(faceScalars) == len(faces): + raise ValueError("Length of faceScalars has to be the same as faces lenght " + "({0}) but is {1}.".format(len(faces), len(faceScalars))) + # demand 2d structure of faceScalars + faceScalars = np.array(faceScalars, dtype=float) + if len(faceScalars.shape) == 1: + faceScalars = faceScalars.reshape(faces.shape[0], -1) + kwargs['faceScalars'] = faceScalars + + super(Mesh3D, cls)._updateSlotKwargs(kwargs, skipCache=skipCache) + + @classmethod + def createFromObjFile(cls, filePath, *groupNames): + """ + Factory method + Given a filePath to a obj/wavefront file, construct the object + """ + ioCls = ioTools.ObjFile + with ioCls(filePath, 'r') as f: + f.process() + vertices, faces = f.getVerticesFaces(*groupNames, firstFace=0) + + log = logger.new() + if len(vertices) == 0: + return cls([]) + faceLenghts = [len(face) for face in faces] + for i in reversed(range(len(faceLenghts))): + length = faceLenghts[i] + if length == 3: + continue + if length == 4: + log.warning("Given a Rectangle. I will split it but " + "sometimes the order is different.") + faces.insert(i + 1, faces[i][2:] + faces[i][:1]) + faces[i] = faces[i][:3] + else: + raise NotImplementedError() + mesh = cls(vertices, faces=faces) + if groupNames: + mesh = mesh.clean() + return mesh + + @classmethod + def createFromInpFile(cls, filePath, **kwargs): + """ + Factory method + Given a filePath to a inp file, construct the object + """ + import transcoding as tc + transcoding = tc.getTranscoding('inp') + content = transcoding.read(filePath) + part = content['parts'][0] + vertices = np.array([part['x'], part['y'], part['z']]).T / 1000 + indices = np.array(part['nodeIndex']) - 1 + if not list(indices) == range(len(indices)): + raise ValueError("node index skipped") + faces = np.array([part['nodeIndex{i}'.format(i=i)] for i in range(3)]).T - 1 + return cls(vertices, faces=faces) + + @classmethod + def createMeshGrid(cls, *baseVectors, **kwargs): + if not len(baseVectors) == 3: + raise AttributeError("3 baseVectors vectors required") + + indices = [0, -1] + coords = range(3) + baseLengthsAbove1 = [len(b) > 1 for b in baseVectors] + # if one plane is given: rearrange indices and coords + if not all(baseLengthsAbove1): + indices = [0] + for i, b in enumerate(baseLengthsAbove1): + if not b: + coords = [i] + break + + baseVectors = list(baseVectors) + mParts = [] + for ind in indices: + for coord in coords: + basePart = baseVectors[:] + basePart[coord] = np.array([baseVectors[coord][ind]], + dtype=float) + + mParts.append(cls.createMeshPlane(*basePart)) + inst = cls.__new__(cls, mParts, **kwargs) + return inst + + @classmethod + def createMeshPlane(cls, *baseVectors, **kwargs): + points = tfields.Points3D.createMeshGrid(*baseVectors) + fixCoord = None + for coord in range(3): + if len(baseVectors[coord]) > 1: + continue + if len(baseVectors[coord]) == 0: + continue + fixCoord = coord + + variableCoords = list(range(3)) + variableCoords.pop(variableCoords.index(fixCoord)) + + faces = [] + base0, base1 = baseVectors[variableCoords[0]], baseVectors[variableCoords[1]] + for i1 in range(len(base1) - 1): + for i0 in range(len(base0) - 1): + pointIdxTopLeft = len(base1) * (i0 + 0) + (i1 + 0) + pointIdxTopRight = len(base1) * (i0 + 0) + (i1 + 1) + pointIdxBotLeft = len(base1) * (i0 + 1) + (i1 + 0) + pointIdxBotRight = len(base1) * (i0 + 1) + (i1 + 1) + faces.append([pointIdxTopLeft, pointIdxTopRight, pointIdxBotLeft]) + faces.append([pointIdxTopRight, pointIdxBotLeft, pointIdxBotRight]) + inst = cls.__new__(cls, points, faces=faces, **kwargs) + return inst + + @decoTools.cached_property() + def triangles(self): + """ + with the decorator, this should be handled like an attribute though it is a function + + """ + if self.faces.size == 0: + return tfields.Triangles3D([]) + return tfields.Triangles3D(self[self.faces.flatten()], triangleScalars=self.faceScalars) + + @decoTools.cached_property() + def planes(self): + if self.faces.size == 0: + return tfields.Planes3D([]) + return tfields.Planes3D(self.getCentroids(), self.getNormVectors()) + + def saveTxt(self, filePath): + if self.coordSys != self.CARTESIAN: + cpy = self.copy() + cpy.coordinateTransform(self.CARTESIAN) + else: + cpy = self + with ioTools.TextFile(filePath, 'w') as f: + matrix = [] + for i, face in enumerate(self.faces): + matrix.append(self.faceScalars[i, :]) + matrix.extend(self[face]) + f.writeMatrix(matrix, seperator=' ', lineBreak='\n') + + @classmethod + def createFromTxtFile(cls, filePath): + return tfields.Triangles3D.createFromTxtFile(filePath).getMesh3D() + + def __getattr__(self, name): + """ + getter methods are forwarded to self.triangles + Examples: + >>> m = tfields.Mesh3D([]); + >>> m.getAreas + <bound method Triangles3D.getAreas of Triangles3D([], shape=(0, 3), dtype=float64)> + + """ + if name.startswith('get'): + if not hasattr(tfields.Triangles3D, name) or name == 'getMask': + raise AttributeError("Could not forward attribute {0}".format(name)) + else: + return getattr(self.triangles, name) + else: + raise AttributeError("No attribute with name {0}".format(name)) + + def getNFaces(self): + return self.faces.shape[0] + + def getMean(self, *args, **kwargs): + """ + Forward this manually since getMean is derived already. + """ + return self.triangles.getMean(*args, **kwargs) + + def getStd(self, *args, **kwargs): + """ + Forward this manually since getStd is derived already. + """ + return self.triangles.getStd(*args, **kwargs) + + def getScalars(self): + return self.faceScalars + + def getScalarArrays(self): + return self.faceScalars.T + + def getScalarDepth(self): + return self.faceScalars.shape[1] + + def setScalarArray(self, scalarIndex, scalarArray): + """ + >>> m = tfields.Mesh3D([[1,2,3], [3,3,3], [0,0,0], [5,6,7]], [[0, 1, 2], [1, 2, 3]], + ... faceScalars=[[1,2,3,4,5], [6,7,8,9,0]]) + >>> m.setScalarArray(1, [42, 84]) + >>> m.faceScalars + array([[ 1., 42., 3., 4., 5.], + [ 6., 84., 8., 9., 0.]]) + + """ + if scalarIndex == self.getScalarDepth(): + self.appendScalars(scalarArray) + else: + self.faceScalars[:, scalarIndex] = scalarArray + # try: + # self.faceScalars[:, scalarIndex] = scalarArray + # except: + # tfields.saveNested("~/tmp/bug1e-6.nest.npz", (self, scalarArray, + # scalarIndex)) + # raise + + def removeScalars(self, scalarIndex=None): + if scalarIndex is not None: + raise NotImplementedError() + self.faceScalars = np.empty(self.faceScalars.shape[0], + dtype=self.faceScalars.dtype) + + def appendScalars(self, scalars): + """ + Similar to list.append but in axis 1 + """ + self.faceScalars = np.concatenate([self.faceScalars, np.array([scalars]).T], 1) + + def stackScalars(self, *stack, **kwargs): + """ + add all faceScalars of stack meshes to self.faceScalars. + Args: + *stack (Mesh3D): input meshes are required to have the + same or simiar faces. This is not tested for time reasons + though. + **kwargs: + mapping (str): + 'centroid': Use centroids for check + which face belongs to which. + 'order': Assume all faces are in the same order. + Examples: + >>> m = tfields.Mesh3D([[1,0,0],[0,1,0],[0,0,1], [0,0,0]], + ... [[0,1,2], [2,3,0], [2,3,1]], + ... faceScalars=[1,2,3]) + >>> c = m.copy() + + If you exactly know, the two meshes are the same: + >>> m.stackScalars(c, mapping='order') + >>> all(m.getScalarArrays()[0, :] == [2.,4.,6.]) + True + + Using the default compares centroids + >>> m = c.copy() + >>> m.stackScalars(c, mapping='centroid') + >>> all(m.getScalarArrays()[0, :] == [2.,4.,6.]) + True + + """ + mapping = kwargs.pop('mapping', 'centroid') + for stackObj in stack: + """ + faceMap is a list of tuples: first meshFaceIndex second + selfFaceIndex + """ + if mapping == 'order': + if not isinstance(stackObj, Mesh3D): + raise NotImplementedError() + faceRange = range(stackObj.getNFaces()) + faceMap = [(i, i) for i in faceRange] + objScalars = stackObj.faceScalars + elif mapping == 'centroid': + if isinstance(stackObj, Mesh3D): + faceRange = range(stackObj.getNFaces()) + centroids = stackObj.getCentroids() + objScalars = stackObj.faceScalars + elif isinstance(stackObj, tfields.ScalarField3D): + faceRange = range(stackObj.getNPoints()) + centroids = stackObj + if mapping != 'centroid': + raise NotImplementedError() + objScalars = stackObj.scalars + else: + raise NotImplementedError() + closestCentroidIndices = \ + centroids.closestPoints(self.getCentroids()) + faceMap = [(i, j) for j, i in zip(faceRange, closestCentroidIndices)] + else: + raise NotImplementedError() + faceMap = np.array(faceMap) + + """ + Rearrange scalars according to faceMap + This is the part that takes longest + """ + scalars = np.full(self.faceScalars.shape, + 0., + dtype=objScalars.dtype) + for i in set(faceMap[:, 0]): + scalars[i] = objScalars[faceMap[faceMap[:, 0] == i, + 1]].sum(axis=0) + + """ + Add all scalars + """ + self.faceScalars = self.faceScalars + scalars + + def toOneSegment(self, mirrorZ=True): + """ + Map the points to the first segment and mirror to positive z + if mirrorZOption is True. Special w7x method + Examples: + Build a mesh in three segments + >>> scalars = np.array([[1,2], [3,4], [5,6], [7,8]], dtype=float) + >>> m = tfields.Mesh3D([[6,-1,1], [6,0,1], [6,1,1], + ... [6,-1,0.5], [6,0,0.5], [6,1,0.5]], + ... [[0, 3, 4], [0, 1, 4], [1,4,5], [1,2,5]], + ... faceScalars=scalars) + >>> c = m.copy() + >>> c.coordinateTransform(c.CYLINDER) + >>> c[:, 1] *= -1 + >>> c[:, 2] *= -1 + + >>> m = tfields.Mesh3D([m, c]) + >>> m2 = m.copy() + >>> m2.toSegment(1) + >>> m3 = m.copy() + >>> m3.toSegment(2) + >>> mAll = tfields.Mesh3D([m, m2, m3]) + >>> mAll.toOneSegment() + >>> bool((mAll.faceScalars == scalars * 6).all()) + True + + """ + with self.tempCoordSys(self.CYLINDER): + dropCut = (-2 * np.pi / 10 < y) & (y < 2 * np.pi / 10) + if mirrorZ: + dropCut = dropCut & (z > 0) + dropCutMask = self.getMask(dropCut) + faceKeepMask = self.getFaceMask(dropCutMask) + excludedMesh = self.copy() + self.removeFaces(~faceKeepMask) + excludedMesh.removeFaces(faceKeepMask) + # remove 0 faces to be faster + from sympy.abc import s + zeroMask = tfields.getMask(excludedMesh.faceScalars, + cutExpression=(s == 0), + coords=[s] * excludedMesh.getScalarDepth()) + excludedMesh.removeFaces(zeroMask) + + # to one segment + super(Mesh3D, self).toOneSegment(mirrorZ=mirrorZ) + centroidSF = tfields.ScalarField3D(excludedMesh.getCentroids(), + excludedMesh.faceScalars) + centroidSF.toOneSegment(mirrorZ=mirrorZ) + + self.stackScalars(centroidSF) + + def pickScalars(self, *scalarIndices): + """ + Reduces the faceScalars to only the indices given. + Examples: + >>> m = tfields.Mesh3D([[1,2,3], [3,3,3], [0,0,0], [5,6,7]], + ... [[0, 1, 2], [1, 2, 3]], + ... faceScalars=[[1,2,3,4,5], [6,7,8,9,0]]) + >>> m.pickScalars(1, 3, 4) + >>> m.faceScalars + array([[ 2., 4., 5.], + [ 7., 9., 0.]]) + + """ + self.faceScalars = self.faceScalars[:, list(scalarIndices)] + + def pointsInMesh(self, points, delta, method='baryc', assignMultiple=False): + """ + Check whether points lie within triangles with Barycentric Technique + """ + masks = self.triangles.pointsInTriangles(points, delta, method='baryc', + assignMultiple=assignMultiple) + return masks + + def convertNaN(self, value=0.): + super(Mesh3D, self).convertNaN(value) + nanIndicesScalars = np.isnan(self.faceScalars) + self.faceScalars[nanIndicesScalars] = value + + def cutScalars(self, cutExpression, coords=None, + replaceValue=np.nan, scalarIndex=None, inplace=False): + """ + Set a threshold to the scalars. + Args: + cutExpression (sympy cut expression or list of those): + threshold(sympy cut expression): cut scalars globaly + threshold(list of sympy cut expressions): set on threshold for every scalar array + Examples: + >>> m = tfields.Mesh3D([[0,0,0], [1,0,0], [0,1,0], [0,0,1]], + ... faces=[[0,1,2], [0,1,3]], + ... faceScalars=[[1, 1], [2, 2]]) + + Cuting all scalars at once + >>> from sympy.abc import s + >>> m.cutScalars(s <= 1., replaceValue=0.).faceScalars + array([[ 0., 0.], + [ 2., 2.]]) + + Cutting scalars different: + >>> m.cutScalars([s <= 1, s >= 2], replaceValue=0.).faceScalars + array([[ 0., 1.], + [ 2., 0.]]) + + Cuttin one special scalar Array only + >>> m.cutScalars(s <= 1, replaceValue=0., scalarIndex=1).faceScalars + array([[ 1., 0.], + [ 2., 2.]]) + + Using a list of cut expressions to cut every scalar index different + + """ + if inplace: + inst = self + else: + inst = self.copy() + + if isinstance(cutExpression, list): + if scalarIndex is not None: + raise ValueError("scalarIndex must be None, " + "if cutExpression is list of cutExpressions") + if not len(cutExpression) == inst.getScalarDepth(): + raise ValueError("lenght of cutExpression must meet scalar depth") + for si, ce in enumerate(cutExpression): + inst.cutScalars(ce, coords=coords, + replaceValue=replaceValue, + scalarIndex=si, inplace=True) + else: + if coords is None: + freeSymbols = cutExpression.free_symbols + if len(freeSymbols) > 1: + raise ValueError('coords must be given if multiple variables are given') + elif len(freeSymbols) == 0: + raise NotImplementedError("Expressiongs like {cutExpression} " + "are not understood for coords".format(**locals())) + coords = list(freeSymbols) * inst.getScalarDepth() + scalarArrays = inst.getScalars() + if scalarIndex is not None: + scalarArrays = scalarArrays[:, scalarIndex:scalarIndex + 1] + + maskBelow = tfields.getMask(scalarArrays, + cutExpression=cutExpression, + coords=[coords[scalarIndex]]) + scalarArrays[maskBelow] = replaceValue + inst.faceScalars[:, scalarIndex:scalarIndex + 1] = scalarArrays + else: + maskBelow = tfields.getMask(scalarArrays, + cutExpression=cutExpression, + coords=coords) + scalarArrays[maskBelow] = replaceValue + inst.faceScalars = scalarArrays + if not inplace: + return inst + + def getFaceMask(self, mask): + """ + Examples: + >>> m = tfields.Mesh3D([[1,2,3], [3,3,3], [0,0,0], [5,6,7]], + ... [[0, 1, 2], [1, 2, 3]], + ... faceScalars=[[1,2,3,4,5], [6,7,8,9,0]]) + >>> from sympy.abc import x,y,z + >>> vertexMask = m.getMask(z < 6) + >>> faceMask = m.getFaceMask(vertexMask) + >>> faceMask + array([ True, False], dtype=bool) + + Returns: + mask of faces with all vertices in mask + """ + faceDeleteMask = np.full((self.faces.shape[0]), False, dtype=bool) + indices = np.array(range(self.getNPoints())) + deleteIndices = set(indices[~mask]) # set speeds up everything + for i, face in enumerate(self.faces): + for index in face: + if index in deleteIndices: + faceDeleteMask[i] = True + break + + return ~faceDeleteMask + + def getRemovedVertices(self, vertexDeleteMask): + """ + Return copy of self without vertices where vertexDeleteMask is True + Copy because self is immutable + + Examples: + >>> m = tfields.Mesh3D([[0,0,0], [1,1,1], [2,2,2], [0,0,0], + ... [3,3,3], [4,4,4], [5,5,5]], + ... [[0, 1, 2], [0, 1, 3], [3, 4, 5], [3, 4, 1], + ... [3, 4, 6]], + ... faceScalars=[[1,2], [3,4], [5,6], [7,8], [9,0]]) + >>> c = m.getRemovedVertices([True, True, True, False, False, + ... False, False]) + >>> c + Mesh3D([[ 0., 0., 0.], + [ 3., 3., 3.], + [ 4., 4., 4.], + [ 5., 5., 5.]]) + >>> c.faces + array([[0, 1, 2], + [0, 1, 3]]) + >>> c.faceScalars + array([[ 5., 6.], + [ 9., 0.]]) + + """ + log = logger.new() + vertexDeleteMask = np.array(vertexDeleteMask) + log.verbose("Remove {0} vertices.".format(vertexDeleteMask.sum())) + # built instance that only contains the vaild points + inst = self[~vertexDeleteMask].copy() + + moveUpCounter = np.zeros(self.faces.shape, dtype=int) + + # correct faces: + deleteIndices = np.arange(self.getNPoints())[vertexDeleteMask] + for p in deleteIndices: + moveUpCounter[self.faces > p] -= 1 + + faceKeepMask = self.getFaceMask(~vertexDeleteMask) + inst.faces = (self.faces + moveUpCounter)[faceKeepMask] + inst.faceScalars = self.faceScalars[faceKeepMask] + return inst + + def cleaned(self, stale=True, duplicates=True): + """ + Args: + stale (bool): remove stale vertices + duplicates (bool): replace duplicate vertices by originals + Examples: + >>> m = tfields.Mesh3D([[0,0,0], [1,1,1], [2,2,2], [0,0,0], + ... [3,3,3], [4,4,4], [5,6,7]], + ... [[0, 1, 2], [3, 4, 5]], + ... faceScalars=[[1,2,3,4,5], [6,7,8,9,0]]) + >>> c = m.clean() + >>> c + Mesh3D([[ 0., 0., 0.], + [ 1., 1., 1.], + [ 2., 2., 2.], + [ 3., 3., 3.], + [ 4., 4., 4.]]) + >>> c.faces + array([[0, 1, 2], + [0, 3, 4]]) + + Returns: + copy of self without stale vertices and duplicat points + """ + log = logger.new() + log.verbose("Cleaning up.") + # remove stale vertices + if stale: + vertexDeleteMask = self.staleVertices() + else: + vertexDeleteMask = np.full(self.shape[0], False, dtype=bool) + # remove duplicates in order to not have any artificial separations + inst = self + if duplicates: + inst = self.copy() + log.verbose("Finding Duplicates") + dups = tfields.duplicates(self, axis=0) + for i, dupi in zip(range(self.shape[0]), dups): + if dupi != i: + log.verbose("Found Duplicate point @ index {0}".format(i)) + vertexDeleteMask[i] = True + # redirect faces + log.verbose("Run trough all faces to let it point to the" + "original") + for f in range(self.getNFaces()): + if i in self.faces[f]: + index = tfields.index(self.faces[f], i) + inst.faces[f][index] = dupi + + return inst.getRemovedVertices(vertexDeleteMask) + + def clean(self, *args, **kwargs): + """ + Deprecated + """ + warnings.warn("Name clean is deprecated. take cleaned instead", DeprecationWarning) + return self.cleaned(*args, **kwargs) + + def getScalarMap(self, mask): + """ + Return copy of self without vertices where mask is True + Copy because self is immutable + """ + # built instance that only contains the vaild points + faceKeepMask = self.getFaceMask(mask) + scalarMap = np.arange(self.getNFaces())[faceKeepMask] + return scalarMap + + def removeFaces(self, faceDeleteMask): + """ + Remove faces where faceDeleteMask is True + Examples: + >>> m = tfields.Mesh3D([[1,2,3], [3,3,3], [0,0,0], [5,6,7]], + ... [[0, 1, 2], [1, 2, 3]], + ... faceScalars=[[1,2], [6,7]]) + >>> m.removeFaces([True, False]) + >>> m.faces + array([[1, 2, 3]]) + + """ + faceDeleteMask = np.array(faceDeleteMask, dtype=bool) + self.faces = self.faces[~faceDeleteMask] + self.faceScalars = self.faceScalars[~faceDeleteMask] + + def keepFaces(self, faceMask=None, faces=None, faceIndices=None): + """ + Inverse method like removeFaces + Args: + faceMask (np.array): + faces (list of list of int) + faceIndices (list of int) + """ + if faces is None: + faces = [] + if faceIndices is None: + faceIndices = [] + if faceMask is None: + faceMask = np.full(self.faces.shape[0], False, dtype=bool) + + for i, face in enumerate(self.faces): + # np. version of if face in faces: + if any((face == f).all() for f in faces): + faceIndices.append(i) + + for ind in faceIndices: + faceMask[ind] = True + + self.removeFaces(~faceMask) + + def staleVertices(self): + """ + Returns: + Mask for all vertices that are stale i.e. are not refered by faces + """ + staleMask = np.full(self.getNPoints(), False, dtype=bool) + used = set(self.faces.flatten()) + for i in range(self.getNPoints()): + if i not in used: + staleMask[i] = True + return staleMask + + def getFaces(self, vertex=None): + """ + Args: + vertex (None / int / array of length 3) + """ + if vertex is None: + return self.faces + if isinstance(vertex, int): + vertex = self[vertex] + if not (isinstance(vertex, list) or isinstance(vertex, np.ndarray)): + raise TypeError("Vertex has wrong type {0}".format(type(vertex))) + index = tfields.index(self, vertex, axis=0) + faces = [] + for face in self.faces: + if index in face: + faces.append(face) + return faces + + def _inputToFaceIndices(self, arg): + """ + convert an input to a faceIndices list + Returns: + list + """ + arg = np.array(arg) + if arg.dtype == bool: + # mask + return np.arange(self.faces.shape[0])[arg] + if len(arg.shape) > 1: + # face + raise NotImplementedError() + else: + return arg + + def _inputToFaceMask(self, arg): + """ + convert an input to a face mask + Returns: + np.array, dtype=bool + """ + arg = np.array(arg) + if arg.dtype == bool: + # mask + return arg + if len(arg.shape) > 1: + # face + raise NotImplementedError() + else: + # faceIndices + tmp = np.full(self.faces.shape[0], False) + tmp[arg] = True + return tmp + + def getParts(self, faceGroupIndicesList): + """ + Args: + faceGroupIndicesList (list of int) + """ + log = logger.new() + faceIndices = range(len(self.faces)) + parts = [] + log.verbose("Run through all {0} groups and partition mesh" + .format(len(faceGroupIndicesList))) + for f, faceGroupIndices in enumerate(faceGroupIndicesList): + log.verbose("Group {0} / {1}".format(f, len(faceGroupIndicesList))) + mesh = self.copy() + # for speed up: + faceGroupIndices = set(faceGroupIndices) + faceDeleteMask = [True + if i not in faceGroupIndices + else False + for i in faceIndices] + mesh.removeFaces(faceDeleteMask) + mesh = mesh.getRemovedVertices(mesh.staleVertices()) + parts.append(mesh) + return parts + + def getLinkedFaces(self, skipFaces=None): + """ + Retrieve the faceIndices that are connected grouped together + Args: + skipFaces: faceSelector (mask, faces, faceIndices) + Returns: + list of list of int: groups of face indices that are linked + + Examples: + >>> import tfields + >>> a = tfields.Mesh3D([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], + ... faces=[[0, 1, 2], [0, 2, 3]]) + >>> b = a.copy() + + >>> b[:, 0] += 2 + >>> m = tfields.Mesh3D([a, b]) + >>> groupIndices = m.getLinkedFaces() + >>> parts = m.getParts(groupIndices) + >>> aa, ba = parts + >>> bool((aa.faces == a.faces).all()) + True + >>> bool((ba.faces == b.faces).all()) + True + >>> bool((aa == a).all()) + True + >>> bool((ba == b).all()) + True + + """ + faces = self.faces + if skipFaces is not None: + mask = ~self._inputToFaceMask(skipFaces) + faces = faces[mask] + faceGroupIndicesList = pyTools.setTools.disjointGroupIndices(faces) + if skipFaces is not None: + faceIndices = np.arange(self.faces.shape[0]) + faceGroupIndicesList = [faceIndices[mask][group] + for group in faceGroupIndicesList] + return faceGroupIndicesList + + def getRegion(self, seedFace, **kwargs): + """ + Grow a region from the seedFace until breaking criterion is reached + Breaking criterion is specified in kwargs + Args: + seedFace (faceMask or faces or faceIndices): + **kwargs: keys: + maxAngle: breaking criterion specified for the normal + vectors not to deviate from neighbours more than maxAngle + Examples: + Get only one side of a cube: + >>> import tfields + >>> import numpy as np + >>> base = [np.linspace(0, 1, 10), + ... np.linspace(0, 1, 10), + ... np.linspace(0, 1, 10)] + >>> mesh = tfields.Mesh3D.createMeshGrid(*base).cleaned() + + Some small mistake occured in the test. Check that. + # Select the first face as a seedFace + # >>> faceGroups = mesh.getRegion([0], maxAngle=np.pi * 2 / 8) + # >>> parts = mesh.getParts(faceGroups) + + # Should only return one group. does not yet -> TODO! + # >>> len(parts) == 1 + + """ + log = logger.new() + if not kwargs: + log.warning("No boundaries specified") + return np.arange(self.faces.shape[0]) + + faceIndices = list(self._inputToFaceIndices(seedFace)) + + # get break condition from kwargs + maxAngle = kwargs.pop('maxAngle', None) + + norms = self.triangles.getNormVectors() + meanVector = np.mean(norms[faceIndices], axis=0) + + excludedFaceIndices = set() + length = 0 + while len(faceIndices) > length: + length = len(faceIndices) + for f, face in enumerate(self.faces): + vertexIndices = list(set(pyTools.flatten(self.faces[faceIndices]))) + for index in vertexIndices: + if index not in face: + continue + if f in faceIndices: + continue + if f in excludedFaceIndices: + continue + norm = norms[f] + angle = np.arccos(np.einsum("...j,...j", meanVector, norm)) + if abs(angle) > maxAngle: + excludedFaceIndices.add(f) + continue + log.verbose("Found new neighbour at face index " + "{f}".format(**locals())) + faceIndices.append(f) + if not len(faceIndices) > length: + log.info("Found no neighbours") + return faceIndices + + def getSides(self, mainAxes=None, deviation=2 * np.pi / 8): + """ + Grouping together face indices that have normal vectors in the + limits of +- deviation or +- pi + deviation. + Examples: + Get only one side of a cube: + >>> import tfields + >>> import numpy as np + >>> base = [np.linspace(0, 1, 2), + ... np.linspace(0, 1, 4), + ... np.linspace(0, 1, 4)] + >>> mesh = tfields.Mesh3D.createMeshGrid(*base).cleaned() + + Select the first face as a seedFace + >>> faceGroups = mesh.getSides([[1,0,0],[0,1,0],[0,0,1]]) + >>> parts = mesh.getParts(faceGroups) + >>> len(parts) == 6 + True + + Faces that have inconsistant norm vector direction are no problem + To show that, we invert the normal vector of one + face in the middle of the cube + >>> mesh.faces[8] = [5, 9, 6] + >>> faceGroups2 = mesh.getSides([[1,0,0],[0,1,0],[0,0,1]]) + >>> parts2 = mesh.getParts(faceGroups2) + >>> len(parts2) == 6 + True + + """ + if mainAxes is None: + axes = self.getMainAxes() + else: + axes = tfields.Points3D(mainAxes) + n = np.apply_along_axis(np.linalg.norm, 0, axes.T).reshape(-1, 1) + axes = axes / n + + norms = self.triangles.getNormVectors() + norms = tfields.Points3D(norms) + + faceGroupIndices = [] + for vector in axes: + angles = np.arccos(np.einsum("...ij,...j", norms, vector)) + mask = np.logical_or(abs(angles) < deviation, + abs(angles - np.pi) < deviation) + tmp = self.getLinkedFaces(skipFaces=~mask) + faceGroupIndices += tmp + return faceGroupIndices + + def getMeshMap(self, subMesh, delta=1e-9): + """ + Returns: + Mesh3D: meshMap (see mapToCut), can be used as meshMap to retrieve + subMesh from self instance + Examples: + >>> m = tfields.Mesh3D([[0,0,0], [1,0,0], [1,1,0], [0,1,0], [0,2,0], [1,2,0]], + ... faces=[[0,1,2],[2,3,0],[3,2,5],[5,4,3]], + ... faceScalars=[[1],[2],[3],[4]]) + >>> from sympy.abc import y + >>> mCut, mapMesh = m.mapToCut(y < 1.5, atIntersection='split') + >>> mm = m.getMeshMap(mCut) + >>> bool((mm == mapMesh).all()) + True + >>> bool((mm.faceScalars == mapMesh.faceScalars).all()) + True + >>> bool((mm.faces == mapMesh.faces).all()) + True + + """ + # log = logger.new() + faceIndices = np.arange(self.faces.shape[0]) + cents = tfields.Points3D(subMesh.getCentroids()) + scalars = [] + # nCents = cents.shape[0] + # for i in range(nCents): + # faceMask = self.pointsInMesh(cents[i: i + 1], delta=delta)[0] + # if True not in faceMask: + # log.warning("No point face was assigned to this. I will retry.") + # faceMask = self.pointsInMesh(cents[i: i + 1], delta=delta * 100)[0] + # if True not in faceMask: + # raise ValueError() + # else: + # log.info("Centroid {i} / {nCents}".format(**locals())) + # scalars.append(faceIndices[faceMask]) + mask = self.pointsInMesh(cents, delta=delta) + scalars = [faceIndices[faceMask] for faceMask in mask] + inst = subMesh.copy() + inst.setScalarArray(0, scalars) + return inst + + + def _distFromPlane(self, point, plane): + return plane['normal'].dot(point) + plane['d'] + + def _getSegmentPlaneIntersection(self, p0, p1, plane): + distance0 = self._distFromPlane(p0, plane) + distance1 = self._distFromPlane(p1, plane) + p0OnPlane = abs(distance0) < np.finfo(float).eps + p1OnPlane = abs(distance1) < np.finfo(float).eps + outPoints = [] + direction = 0 + if p0OnPlane: + outPoints.append(p0) + + if p1OnPlane: + outPoints.append(p1) + # remove duplicate points + if len(outPoints) > 1: + outPoints = tfields.Points3D(np.unique(outPoints, axis=0)) + if p0OnPlane and p1OnPlane: + return outPoints, direction + + if distance0 * distance1 > np.finfo(float).eps: + return outPoints, direction + + direction = np.sign(distance0) + if abs(distance0) < np.finfo(float).eps: + return outPoints, direction + elif abs(distance1) < np.finfo(float).eps: + return outPoints, direction + if abs(distance0 - distance1) > np.finfo(float).eps: + t = distance0 / (distance0 - distance1) + else: + return outPoints, direction + + outPoints.append(p0 + t * (p1 - p0)) + # remove duplicate points + if len(outPoints) > 1: + outPoints = tfields.Points3D(np.unique(outPoints, axis=0)) + return outPoints, direction + + def _intersect(self, triangle, plane, facePointsRejected): + nTrue = facePointsRejected.count(True) + lonelyBool = True if nTrue == 1 else False + index = facePointsRejected.index(lonelyBool) + s0, d0 = self._getSegmentPlaneIntersection(triangle[0], triangle[1], plane) + s1, d1 = self._getSegmentPlaneIntersection(triangle[1], triangle[2], plane) + s2, d2 = self._getSegmentPlaneIntersection(triangle[2], triangle[0], plane) + + singlePoint = triangle[index] + couplePoints = [triangle[j] for j in range(3) + if not facePointsRejected[j] == lonelyBool] + + # TODO handle special cases. For now triangles with at least two points on plane are excluded + newPoints = None + faces = None + + if len(s0) == 2: + # both points on plane + return None, None + if len(s1) == 2: + # both points on plane + return None, None + if len(s2) == 2: + # both points on plane + return None, None + if lonelyBool: + # one new triangle + if len(s0) == 1 and len(s1) == 1: + newPoints = np.array([couplePoints[0], couplePoints[1], s0[0], s1[0]]) + faces = np.array([[0, 2, 1], [1, 2, 3]]) + elif len(s1) == 1 and len(s2) == 1: + newPoints = np.array([couplePoints[0], couplePoints[1], s1[0], s2[0]]) + faces = np.array([[0, 1, 2], [0, 2, 3]]) + elif len(s0) == 1 and len(s2) == 1: + newPoints = np.array([couplePoints[0], couplePoints[1], s0[0], s2[0]]) + faces = np.array([[0, 1, 2], [1, 3, 2]]) + else: + return None, None + + else: + # two new triangles + if len(s0) == 1 and len(s1) == 1: + newPoints = np.array([singlePoint, s0[0], s1[0]]) + faces = np.array([[0, 2, 1]]) + elif len(s1) == 1 and len(s2) == 1: + newPoints = np.array([singlePoint, s1[0], s2[0]]) + faces = np.array([[0, 2, 1]]) + elif len(s0) == 1 and len(s2) == 1: + newPoints = np.array([singlePoint, s0[0], s2[0]]) + faces = np.array([[0, 1, 2]]) + else: + return None, None + return newPoints, faces + + def mapToCut(self, cutOrMeshMap, coordSys=None, atIntersection="remove"): + """ + Partition the mesh with the cuts given + Args: + cutOrMeshMaps (cutExpression or meshMap): + if cutOrMeshMap is a cutExpression: cut like in self.cut but also return + a map from new to old faces + if cutOrMeshMap is a meshMap: map without cutting according to the map given + coordSys (str): coordSys for cutExpression + atIntersection (str): option switch for cutExpression (see + tfields.Mesh3D.cut) + Examples: + Cut away the first face / select the last face only + >>> m = tfields.Mesh3D([[1,2,3], [3,3,3], [0,0,0], [5,6,7]], + ... faces=[[0, 1, 2], [1, 2, 3]], + ... faceScalars=[[1,2], [6,7]]) + >>> mNew = tfields.Mesh3D([[3,3,3], [0,0,0], [5,6,7]], [[0, 1, 2]], faceScalars=[[1]]) + >>> mCut, _ = m.mapToCut(mNew) + >>> mCut.faceScalars + array([[ 6., 7.]]) + + Cut with condition will return the manual/instruction on how to + conduct the cut fast (mapMesh) + >>> from sympy.abc import y + >>> m = tfields.Mesh3D([[0,0,0], [1,0,0], [1,1,0], [0,1,0], [0,2,0], [1,2,0]], + ... faces=[[0,1,2],[2,3,0],[3,2,5],[5,4,3]], + ... faceScalars=[[1],[2],[3],[4]]) + >>> mCut, mapMesh = m.mapToCut(y < 1.5, atIntersection='split') + >>> mCut.faceScalars.T + array([[ 1., 2., 3., 3., 4.]]) + + Applying the mapMesh results in the same result as applying the cut + with the condition + >>> mCut2, _ = m.mapToCut(mapMesh) + >>> bool((mCut == mCut2).all()) + True + >>> bool((mCut.faceScalars == mCut2.faceScalars).all()) + True + + """ + cutExpression = None + meshMap = None + # check what cutOrMeshMap is and set it + if isinstance(cutOrMeshMap, Mesh3D): + meshMap = cutOrMeshMap + else: + cutExpression = cutOrMeshMap + + if cutExpression is not None: + eps = 0.000000001 + # direct return if self is empty + if len(self) == 0: + return self.copy(), self.copy() + + # mask for points that do not fulfill the cut expression + mask = self.getMask(cutExpression, coordSys=coordSys) + # remove the points + inst = self.getRemovedVertices(~mask) + scalarMap = self.getScalarMap(mask) + scalarMap = scalarMap.reshape((-1, 1)).astype(float) + + if not any(~mask): + # no faces have been removed. + return inst, Mesh3D([inst], faceScalars=scalarMap) + + if all(~mask): + # all faces have been removed) + return inst, inst.copy() + + # add points and faces intersecting with the plane + if atIntersection == 'split' or atIntersection == 'splitRough': + cutExpressionParts = symTools.getExpressionParts(cutExpression) + if len(cutExpressionParts) > 1: + onEdgeMesh = self.copy() + if atIntersection == 'splitRough': + """ + the following is, to speed up the process. Problem is, that + triangles can exist, where all points lie outside the cut, + but part of the area + still overlaps with the cut. + These are at the intersection line between two cuts. + """ + faceIntersMask = np.full((self.faces.shape[0]), False, dtype=bool) + for i, face in enumerate(self.faces): + facePointsRejected = [-mask[f] for f in face] + faceOnEdge = any(facePointsRejected) and not all(facePointsRejected) + if faceOnEdge: + faceIntersMask[i] = True + onEdgeMesh.removeFaces(-faceIntersMask) + + for exprPart in cutExpressionParts: + onEdgeMesh = onEdgeMesh.cut(exprPart, atIntersection='split') + newMesh = onEdgeMesh + elif len(cutExpressionParts) == 1: + + points = [sympy.symbols('x0, y0, z0'), sympy.symbols('x1, y1, z1'), sympy.symbols('x2, y2, z2')] + fpr = sympy.symbols('fpr2, fpr1, fpr2') + planeSympy = symTools.getPlane(cutExpression) + n = np.array(planeSympy.normal_vector).astype(float) + d = -n.dot(np.array(planeSympy.p1).astype(float)) + plane = {'normal': n, 'd': d} + + normVectors = self.getNormVectors() + newPoints = np.empty((0, 3)) + newFaces = np.empty((0, 3)) + newFaceScalars = [] + newNormVectors = [] + newScalarMap = [] + nNew = 0 + for i, face in enumerate(self.faces): + """ + facePointsRejected is a mask for each face that is True, where + a Point is on the rejected side of the plane + """ + facePointsRejected = [~mask[f] for f in face] + faceOnEdge = any(facePointsRejected) and not all(facePointsRejected) + if faceOnEdge: + """ + define the two lines that are intersecting the plane: + one point will be alone on one side of the cutting plane (singlePoint) + the other two will be on ther other side (couplePoints) + """ + nTrue = facePointsRejected.count(True) + # the bool that occures on + lonelyBool = True if nTrue == 1 else False + + triangle_points = [self[f] for f in face] + """ + Add the intersection points and faces + """ + # tick = time() + if lonelyBool: + # singlePoint on cut away side + newP, newF = self._intersect(triangle_points, plane, facePointsRejected) + if newP is not None: + newP = np.array(newP) + np.place(newP, np.logical_and(0 < newP, newP <= eps), 0.0) + newPoints = np.concatenate((newPoints, newP)) + newFaces = np.concatenate((newFaces, newF + nNew)) + newFaceScalars.extend([self.faceScalars[i]] * 2) + newNormVectors.extend([normVectors[i]] * 2) + newScalarMap.extend([i] * 2) + nNew += 4 + else: + pass + else: + # singlePoint on keep side + newP, newF = self._intersect(triangle_points, plane, facePointsRejected) + if newP is not None: + newP = np.array(newP) + np.place(newP, np.logical_and(0 < newP, newP <= eps), 0.0) + newPoints = np.concatenate((newPoints, newP)) + newFaces = np.concatenate((newFaces, newF + nNew)) + newFaceScalars.extend([self.faceScalars[i]] * 1) + newNormVectors.extend([normVectors[i]] * 1) + newScalarMap.extend([i]) + nNew += 3 + else: + pass + + newPoints = [[float(p[0]), float(p[1]), float(p[2])] for p in newPoints] + newMesh = self.__class__(newPoints, faces=newFaces, + coordSys=inst.getCoordSys(), + faceScalars=newFaceScalars) + newMesh.alignNormVectors(newNormVectors) + newScalarMap = np.array(newScalarMap).reshape((-1, 1)).astype(float) + scalarMap = np.concatenate((scalarMap, newScalarMap)) + inst = self.__class__([inst, newMesh]) + else: + raise ValueError("CutExpression is not splitable.") + + elif atIntersection == 'remove': + pass + else: + raise AttributeError("No atIntersection method called {atIntersection} " + "implemented".format(**locals())) + meshMap = inst.copy() + meshMap.faceScalars = scalarMap + elif meshMap is not None: + if coordSys is not None or atIntersection != "remove": + log = logger.new() + log.warning("coordSys and atIntersection arguments are not default.") + if len(meshMap) > 0: + inst = Mesh3D(meshMap, + faces=meshMap.faces, + faceScalars=self.faceScalars[meshMap.getScalarArrays()[0].astype(int)], + coordSys=meshMap.coordSys) + else: + inst = Mesh3D([]) + else: + raise TypeError("cutOrMeshMap is not understood.") + return inst, meshMap + + def cut(self, cutExpression, coordSys=None, atIntersection="remove"): + """ + cut method for Mesh3D. + If the same cut to the same mesh should be applied multiple times with different scalars: + use mapToCut! + Args: + see Points3D.cut + additionally: + + atIntersection (str): instruction on what to do, when a cut will intersect a triangle. + Options: "remove" (Default) + "split" - Create new triangles that make up the old one. + + Examples: + define the cut + >>> from sympy.abc import x,y,z + >>> cutExpr = x > 1.5 + + >>> m = tfields.Mesh3D.createMeshGrid(np.linspace(0,3,4), + ... np.linspace(0,3,4), + ... np.linspace(0, 0, 1), faceScalars=np.linspace(4, 8, 18)) + >>> mNew = m.cut(cutExpr) + >>> mNew.getNPoints() + 8 + >>> mNew.getNFaces() + 6 + >>> float(mNew[:, 0].min()) + 2.0 + + Cutting with the split option will create new triangles on the edge: + >>> mSplit = m.cut(cutExpr, atIntersection='split') + >>> float(mSplit[:, 0].min()) + 1.5 + >>> mSplit.getNPoints() + 29 + >>> mSplit.getNFaces() + 15 + + Cuttin with a sympy BooleanFunction: + >>> cutExprBoolFun = (x > 1.5) & (y < 1.5) & (y >0.2) & (z > -0.5) + >>> mSplit = m.cut(cutExprBoolFun, atIntersection='split') + + Returns: + copy of cut mesh + + """ + inst, _ = self.mapToCut(cutExpression, coordSys=coordSys, atIntersection=atIntersection) + return inst + + def _clusterDbscan(self, maxClusterDist=15, minSamples=20, outType=None): + """ + runs DBSCAN alcorithm to find seperate mesh clusters + KwArgs: + outType (type): type of cluster-points. E.g. np.array / Points3D / list... + Returns: + clusters (list of pointsLists), excludedPoints (pointsList) + """ + raise NotImplementedError("") + clusters = [None] + excluded = None + return clusters, excluded + + def _clusterBox(self, + min0=None, max0=None, nSections0=None, + min1=None, max1=None, nSections1=None, + min2=None, max2=None, nSections2=None, + clusterMeshes=None, outType=None, cutTree=False, withTransform=False, **cutKwargs): + log = logger.new() + + if withTransform: + targetMesh, transformation, translate = self.transformDirections() + else: + targetMesh = self + transformation = np.eye(3) + translate = np.zeros(3, ) + + if cutTree and (clusterMeshes is None): + minmax = np.array([[min0, min1, min2], [max0, max1, max2]]) + xRange = list(np.linspace(minmax[0,0], minmax[1,0], nSections0 + 1))[1:-1] + yRange = list(np.linspace(minmax[0,1], minmax[1,1], nSections1 + 1))[1:-1] + zRange = list(np.linspace(minmax[0,2], minmax[1,2], nSections2 + 1))[1:-1] + cuts = {'x': xRange, 'y': yRange, 'z': zRange} + box = {'x': minmax[:,0], 'y': minmax[:,1], 'z': minmax[:,2]} + log.info("Start creating cutting tree") + atIntersection = cutKwargs.get('atIntersection', 'remove') + cutTree = cuttingTree.Node(parent=None, mesh=targetMesh, remainingCuts=cuts, + atIntersection=atIntersection, box=box) + log.info("Done building the tree structure") + + leaves = cutTree.findLeaves() + leaves = cuttingTree.Node.sortLeaves(leaves) + + clusterMeshes = [leaf.getGlobalCutMesh() for leaf in leaves] + clusteredObjs = [leaf.mesh for leaf in leaves] + + # TODO exclude Mesh + excludedObjs = Mesh3D([]) + else: + clusteredObjs, excludedObjs, clusterMeshes, transformation = super(Mesh3D, self)._clusterBox(min0=min0, max0=max0, + nSections0=nSections0, + min1=min1, max1=max1, + nSections1=nSections1, + min2=min2, max2=max2, + nSections2=nSections2, + clusterMeshes=clusterMeshes, + outType=outType, + withTransform=withTransform, + **cutKwargs) + return clusteredObjs, excludedObjs, clusterMeshes, transformation + + def _minimum_bounding_rectangle(self, points2D): + """ + Find the smallest bounding rectangle for a set of points. + Returns a set of points representing the corners of the bounding box. + + :param points: an nx2 matrix of coordinates + :rval: an nx2 matrix of coordinates + """ + from scipy.ndimage.interpolation import rotate + pi2 = np.pi / 2. + + # get the convex hull for the points + hull_points = points2D[ConvexHull(points2D).vertices] + + # calculate edge angles + edges = np.zeros((len(hull_points) - 1, 2)) + edges = hull_points[1:] - hull_points[:-1] + + angles = np.zeros((len(edges))) + angles = np.arctan2(edges[:, 1], edges[:, 0]) + + angles = np.abs(np.mod(angles, pi2)) + angles = np.unique(angles) + + # find rotation matrices + # XXX both work + rotations = np.vstack([ + np.cos(angles), + np.cos(angles - pi2), + np.cos(angles + pi2), + np.cos(angles)]).T + # rotations = np.vstack([ + # np.cos(angles), + # -np.sin(angles), + # np.sin(angles), + # np.cos(angles)]).T + rotations = rotations.reshape((-1, 2, 2)) + + # apply rotations to the hull + rot_points = np.dot(rotations, hull_points.T) + + # find the bounding points + min_x = np.nanmin(rot_points[:, 0], axis=1) + max_x = np.nanmax(rot_points[:, 0], axis=1) + min_y = np.nanmin(rot_points[:, 1], axis=1) + max_y = np.nanmax(rot_points[:, 1], axis=1) + + # find the box with the best area + areas = (max_x - min_x) * (max_y - min_y) + best_idx = np.argmin(areas) + + # return the best box + x1 = max_x[best_idx] + x2 = min_x[best_idx] + y1 = max_y[best_idx] + y2 = min_y[best_idx] + r = rotations[best_idx] + + rval = np.zeros((4, 2)) + rval[0] = np.dot([x1, y2], r) + rval[1] = np.dot([x2, y2], r) + rval[2] = np.dot([x2, y1], r) + rval[3] = np.dot([x1, y1], r) + + return rval, r + + def transformDirections(self): + """ + Transforms the coordinate system such that it faces towards the mean of all triangle normals. + This only works if the triangle normals mean is a reasonable choice for the problem. + An improved version in the future should consider to weight the normals by triangle area + The minimal value in this coordinate system is 0, the maximal value is 1 + :param inMesh: input mesh that the transformation is based on + :return: normalized mesh with transformed vertices, faces and faceScalars as in inMesh, + tranformationMatrix and translate transforms the normalizedMesh back to its original shape + """ + + zNormal = np.array([0.0, 0.0, 1.0]) + meanNormal = np.mean(self.getNormVectors(), axis=0) + meanNormal = meanNormal / np.linalg.norm(meanNormal) + + cosMeanNormalZ = meanNormal.dot(zNormal) + if cosMeanNormalZ == -1.0: + # mean normal points in opposite direction as zNormal + rotateMeanNormal = np.diag([1.0, 1.0, -1.0]) + elif cosMeanNormalZ == 1.0: + # already in z direction + rotateMeanNormal = np.eye(3) + else: + # calculate transformation matrix to let meanNormal be transformed to zNormal + normalsCrossProduct = np.cross(zNormal, meanNormal) + crossProductNorm = np.linalg.norm(normalsCrossProduct) + cosMeanNormalZ = meanNormal.dot(zNormal) + # skew-symmetric cross product matrix of normalsCrossProduct + nux = np.array([[0, -normalsCrossProduct[2], normalsCrossProduct[1]], + [normalsCrossProduct[2], 0, -normalsCrossProduct[0]], + [-normalsCrossProduct[1], normalsCrossProduct[0], 0]]) + # formula found at + # https://math.stackexchange.com/questions/180418/calculate-rotation-matrix-to-align-vector-a-to-vector-b-in-3d + rotateMeanNormal = np.eye(3) + nux + nux.dot(nux) * (1 - cosMeanNormalZ) / (crossProductNorm ** 2) + vertices3D = self.dot(rotateMeanNormal) + unrotatedMesh = Mesh3D(vertices3D, faces=self.faces, faceScalars=self.faceScalars) + + vertices2D = vertices3D[:, :2] + boundingRectangleVertices2D, rot = self._minimum_bounding_rectangle(vertices2D) + + rotatedRectangle = boundingRectangleVertices2D.dot(rot.T) + + # longer side rotated parallel to x-axis + side0 = np.linalg.norm(rotatedRectangle[0, :] - rotatedRectangle[1, :]) + side1 = np.linalg.norm(rotatedRectangle[0, :] - rotatedRectangle[3, :]) + if side0 < side1: + rot = rot.dot(np.array([[0, -1], [1, 0]])) + rotMatrix3D = np.eye(3) + rotMatrix3D[:2, :2] = rot.T + + vertices3D = vertices3D.dot(rotMatrix3D) + + # find mins and max for scaling + mins = np.min(vertices3D, axis=0) + maxs = np.max(vertices3D, axis=0) + # scaling matrix + scale = np.diag([1.0 / (ma - mi) if (abs(ma - mi) > np.finfo(float).eps) else 1.0 + for ma, mi in zip(maxs, mins)]) + # translation vector + translate = np.array([mi / (mi - ma) if (abs(ma - mi) > np.finfo(float).eps) else mi + for ma, mi in zip(maxs, mins)]) + normalizedVertices = vertices3D.dot(scale) + translate + normalizedMesh = Mesh3D(normalizedVertices, faces=self.faces, faceScalars=self.faceScalars) + tranformationMatrix = rotateMeanNormal.dot(rotMatrix3D.dot(scale)) + + return normalizedMesh, tranformationMatrix, translate + + def alignNormVectors(self, normVectors): + """ + Orientate the faces such, that their normVectors align to the normVectors given. + Examples + >>> m = tfields.Mesh3D([[0,0,0], [1,0,0], [-1,0,0], [0,1,0], [0,0,1]], + ... [[0, 1, 3], [1, 3, 4], [1, 3, 2]]); + >>> newNorms = m.getNormVectors() * -1 + >>> m.alignNormVectors(newNorms) + >>> m.faces + array([[0, 3, 1], + [1, 4, 3], + [1, 2, 3]]) + + """ + if not self.getNFaces() == 0: + # vector product < 0 + mask = np.einsum('...i,...i', self.getNormVectors(), normVectors) < 0 + """ + the line: + " self.faces[:, [1, 2]][mask] = self.faces[:, [2, 1]][mask] " + would be a nice solution, but numpy does not mutate the [1, 2] but returns a copy + + """ + temp = np.copy(self.faces[mask, 1]) + self.faces[mask, 1] = self.faces[mask, 2] + self.faces[mask, 2] = temp + + def saveObj(self, filePath, **kwargs): + """ + Save obj as wavefront/.obj file + """ + obj = kwargs.pop('object', None) + group = kwargs.pop('group', None) + + cmap = kwargs.pop('cmap', 'jet') + faceScalarIndex = kwargs.pop('faceScalarIndex', None) + + filePath = filePath.replace('.obj', '') + fileDir, fileName = os.path.split(filePath) + + if not (self.faceScalars.size == 0 or faceScalarIndex is None): + scalars = self.faceScalars[:, faceScalarIndex] + minScalar = scalars[-np.isnan(scalars)].min() + maxScalar = scalars[-np.isnan(scalars)].max() + vmin = kwargs.pop('vmin', minScalar) + vmax = kwargs.pop('vmax', maxScalar) + if vmin == vmax: + if vmin == 0.: + vmax = 1. + else: + vmin = 0. + norm = colors.Normalize(vmin, vmax) + colorMap = plt.get_cmap(cmap) + else: + # this is a switch for not coloring the triangles and thus not producing the materials + norm = None + + if norm is not None: + fileNameMat = fileName + '_frame_{0}.mat'.format(faceScalarIndex) + scalars[np.isnan(scalars)] = minScalar - 1 + sortedScalars = scalars[scalars.argsort()] + sortedScalars[sortedScalars == minScalar - 1] = np.nan + sortedFaces = self.faces[scalars.argsort()] + scalarSet = np.unique(sortedScalars) + scalarSet[scalarSet == minScalar - 1] = np.nan + with open(os.path.join(fileDir + fileNameMat), 'w') as mf: + for s in scalarSet: + if np.isnan(s): + mf.write("newmtl nan") + mf.write("Kd 0 0 0\n\n") + else: + mf.write("newmtl mtl_{0}\n".format(s)) + mf.write("Kd {c[0]} {c[1]} {c[2]}\n\n".format(c=colorMap(norm(s)))) + else: + sortedFaces = self.faces + + # writing of the obj file + with open(filePath + '.obj', 'w') as f: + f.write("# File saved with tfields Mesh3D.saveObj method\n\n") + if norm is not None: + f.write("mtllib ./{0}\n\n".format(fileNameMat)) + if obj is not None: + f.write("o {0}\n".format(obj)) + if group is not None: + f.write("g {0}\n".format(group)) + for vertex in self: + f.write("v {v[0]} {v[1]} {v[2]}\n".format(v=vertex)) + + lastScalar = None + for i, face in enumerate(sortedFaces + 1): + if norm is not None: + if not lastScalar == sortedScalars[i]: + lastScalar = sortedScalars[i] + f.write("usemtl mtl_{0}\n".format(lastScalar)) + f.write("f {f[0]} {f[1]} {f[2]}\n".format(f=face)) + + def plot(self, **kwargs): + """ + Frowarding to plotTools.plotMesh + """ + # old version with plotTools + # import plotTools as pt + # return pt.plotMesh(self, **kwargs) + + plotVertices = kwargs.pop('plotVertices', False) + faceScalarIndex = kwargs.pop('faceScalarIndex', None) + if faceScalarIndex is not None: + warnings.warn("faceScalarIndex is renamed scalarIndex. Also default is not 0 any more.", + DeprecationWarning) + kwargs['scalarIndex'] = kwargs.pop('scalarIndex', faceScalarIndex) + + scalarsDemanded = any([v in kwargs for v in ['vmin', 'vmax', 'cmap']]) + defaultScalarIndex = None if not scalarsDemanded else 0 + # defaultScalarIndex = None + scalarIndex = kwargs.pop('scalarIndex', defaultScalarIndex) + if scalarIndex is not None: + if not self.faceScalars.shape[0] == 0: + kwargs['color'] = self.faceScalars[:, scalarIndex] + if plotVertices: + return mplTools.plotArray(self, **kwargs) + # deprecated dim=3 default. I want to go for 2 as everywhere + dimDefined = False + if 'axis' in kwargs: + dimDefined = True + if 'zAxis' in kwargs: + if kwargs['zAxis'] is not None: + kwargs['dim'] = 3 + else: + kwargs['dim'] = 2 + dimDefined = True + if 'dim' in kwargs: + dimDefined = True + + if not dimDefined: + warnings.warn("Dimension is not defined. I want to change" + " default dimension to 2. Keep 3 for now. " + "If you want 3 specify keyword 'dim'.", + DeprecationWarning) + kwargs['dim'] = 3 + + return mplTools.plotMesh(self, self.faces, **kwargs) + + +if __name__ == '__main__': + import doctest + + # doctest.run_docstring_examples(Mesh3D.clean, globals()) + doctest.testmod() diff --git a/tfields/planes3D.py b/tfields/planes3D.py new file mode 100644 index 0000000000000000000000000000000000000000..8d7dab3a9628fa0e142bd1253d3a3961d474ead2 --- /dev/null +++ b/tfields/planes3D.py @@ -0,0 +1,48 @@ +#!/usr/bin/env +# encoding: utf-8 +""" +Author: Daniel Boeckenhoff +Mail: daniel.boeckenhoff@ipp.mpg.de + +part of npTools library +""" +import npTools +import sympy +import mplTools as mpt +import loggingTools +logger = loggingTools.Logger(__name__) + + +class Planes3D(npTools.VectorField3D): + """ + Point-NormVector representaion of planes + """ + + def symbolic(self): + """ + Returns: + list: list with sympy.Plane objects + """ + return [sympy.Plane(point, normal_vector=vector) + for point, vector in zip(self.points, self.vectors)] + + def plot(self, **kwargs): + """ + forward to Mesh3D plotting + """ + artists = [] + for i in range(self.points.shape[0]): + artists.append(mpt.plotPlane(self.points[0], + self.vectors[0], + **kwargs)) + # symbolic = self.symbolic() + # planeMeshes = [npTools.Mesh3D([pl.arbitrary_point(t=(i + 1) * 1. / 2 * np.pi) + # for i in range(4)], + # faces=[[0, 1, 2], [1, 2, 3], [2, 3, 0]]) for pl in symbolic] + # artists = [m.plot(**kwargs) for m in planeMeshes] + return artists + + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/tfields/scalarField3D.py b/tfields/scalarField3D.py new file mode 100644 index 0000000000000000000000000000000000000000..8926a77f091d2d74011e6094e5fb217765edbdc0 --- /dev/null +++ b/tfields/scalarField3D.py @@ -0,0 +1,346 @@ +#!/usr/bin/env +# encoding: utf-8 +""" +Author: Daniel Boeckenhoff +Mail: daniel.boeckenhoff@ipp.mpg.de + +part of npTools library +""" +import numpy as np +import npTools +import loggingTools +logger = loggingTools.Logger(__name__) + + +class ScalarField3D(npTools.Points3D): + """Class to Store scalar Fields in 3D. + Multiple scalars can be saved. Thus scalars is a multidimensional array. + + Note: + + Args: + inputArray: see Points3D + scalars (list like object) + + Attributes: + x1 (list): coordinate x + x2 (list): coordinate y + x3 (list): coordinate z + scalars (list, can be list of lists.) + + Examples: + No need to fille a scalar to the field + >>> empty = npTools.ScalarField3D([[1,2,3], [4,5,6], [7,8,9]]) + >>> empty.scalars + array([], shape=(3, 0), dtype=float64) + + Multiple scalars also ok. + >>> s = npTools.ScalarField3D.createTest() + >>> s + points: [[ 3. 0. 6.] + [ 3. 0. 9.]] + scalars: [[ 0. 2. 0.] + [ 1. 2. 3.]] + + Saving and reading works + >>> from tempfile import NamedTemporaryFile + >>> outFile = NamedTemporaryFile(suffix='.npz') + >>> s.savez(outFile.name) + >>> _ = outFile.seek(0) + >>> s1 = npTools.ScalarField3D.createFromFile(outFile.name) + >>> bool(np.all(s == s1)) + True + >>> s1.scalars + array([[ 0., 2., 0.], + [ 1., 2., 3.]]) + + Adding scalar fields + >>> s1[:, 2] = 42 + >>> s1.scalars[:, 0] = 24 + >>> s2 = npTools.ScalarField3D([s, s1, s]) + >>> s2 + points: [[ 3. 0. 6.] + [ 3. 0. 9.] + [ 3. 0. 42.] + [ 3. 0. 42.] + [ 3. 0. 6.] + [ 3. 0. 9.]] + scalars: [[ 0. 2. 0.] + [ 1. 2. 3.] + [ 24. 2. 0.] + [ 24. 2. 3.] + [ 0. 2. 0.] + [ 1. 2. 3.]] + + >>> s3 = npTools.ScalarField3D([s, s1, s], scalars = [[0], [1], [2], [3], [4], [5]]) + >>> s3 + points: [[ 3. 0. 6.] + [ 3. 0. 9.] + [ 3. 0. 42.] + [ 3. 0. 42.] + [ 3. 0. 6.] + [ 3. 0. 9.]] + scalars: [[0] + [1] + [2] + [3] + [4] + [5]] + + """ + __slots__ = ['coordSys', 'scalars'] + + def __new__(cls, inputArray, scalars=None, **kwargs): + + if any([isinstance(a, ScalarField3D) for a in inputArray]) and scalars is None: + if not all([isinstance(a, ScalarField3D) for a in inputArray]): + # TODO: could allow if all scalars are none + raise TypeError("Copy constructor of ScalarField3D only accepts" + "ScalarField3D instances.") + if not len(set([o.scalars.shape for o in inputArray])) == 1: + raise TypeError("Shape of scalars that should be concatenated must be the same.") + scalars = np.concatenate([o.scalars for o in inputArray], axis=0) + + obj = npTools.Points3D.__new__(cls, inputArray, **kwargs) + + # add scalars + if scalars is None: + scalars = np.empty((len(obj), 0)) + scalars = np.array(scalars) + if not len(scalars) == len(obj): + raise ValueError("Length of scalars ({0}) has to be the same as " + "points length ({1}) but is not.".format(len(scalars), len(obj))) + obj.scalars = scalars + return obj + + def __repr__(self): + return "points: %s\nscalars: %s" % (self, self.scalars) + + def cut(self, cutExpression, coordSys=None): + """ + Examples: + >>> import npTools + >>> x, y, z = sympy.symbols('x y z') + >>> p = npTools.ScalarField3D([[1., 2., 3.], [4., 5., 6.], [1, 2, -6], + ... [-5, -5, -5], [1,0,-1], [0,1,-1]], scalars = [1,2,3,4,5,6]) + >>> p.cut(x > 0) + points: [[ 1. 2. 3.] + [ 4. 5. 6.] + [ 1. 2. -6.] + [ 1. 0. -1.]] + scalars: [1 2 3 5] + + cut combination + >>> p.cut((x > 0) & (z < 0)) + points: [[ 1. 2. -6.] + [ 1. 0. -1.]] + scalars: [3 5] + + """ + mask = self.getMask(cutExpression, coordSys=coordSys) + + inst = self[mask].copy() + inst.scalars = inst.scalars[mask] + + return inst + + @classmethod + def createTest(cls): + p = npTools.MeshGrid3D(3, 4, 1, 0, 2, 1, 6, 9, 2, iterOrder=[1, 0, 2], coordSys=cls.CYLINDER) + scalars = [[i * 1., 2, 3 * i] for i in range(len(p))] + return cls(p, scalars) + + def evalAtPoint(self, point): + """ + Examples: + >>> s = npTools.ScalarField3D.createTest() + >>> s.evalAtPoint(npTools.Points3D([[3, 0, 6]])) + array([[ 0., 2., 0.]]) + >>> s.evalAtPoint([3, 0, 9]) + array([[ 1., 2., 3.]]) + >>> s.evalAtPoint([[0, 0, 0]]) + array([], shape=(0, 3), dtype=float64) + + """ + return self.scalars[np.equal(self, point).all(axis=1)] + + def getScalarDepth(self): + return self.scalars.shape[1] + + def getScalarArrays(self): + """ + Examples: + >>> s = npTools.ScalarField3D.createTest() + >>> s.getScalarArrays() + array([[ 0., 1.], + [ 2., 2.], + [ 0., 3.]]) + + """ + return self.scalars.T + + def setScalarArray(self, scalarIndex, scalarArray): + """ + Examples: + >>> s = npTools.ScalarField3D.createTest() + >>> s.setScalarArray(1, [42, 84]) + >>> s.getScalarArrays() + array([[ 0., 1.], + [ 42., 84.], + [ 0., 3.]]) + + >>> s.setScalarArray(3, [-1, -1]) + >>> s.getScalarArrays() + array([[ 0., 1.], + [ 42., 84.], + [ 0., 3.], + [ -1., -1.]]) + + """ + if scalarIndex == self.getScalarDepth(): + self.appendScalars(scalarArray) + self.scalars[:, scalarIndex] = np.array(scalarArray) + + def appendScalars(self, scalars): + """ + Examples: + >>> s = npTools.ScalarField3D.createTest() + >>> s.appendScalars([42, 84]) + >>> s.getScalarArrays() + array([[ 0., 1.], + [ 2., 2.], + [ 0., 3.], + [ 42., 84.]]) + + """ + self.scalars = np.concatenate([self.scalars, np.array([scalars]).T], 1) + + def dropScalars(self, indices=None): + """ + Examples: + >>> s = npTools.ScalarField3D.createTest() + >>> s.appendScalars([42, 84]) + + give index + >>> s.dropScalars(-2) + >>> s.getScalarArrays() + array([[ 0., 1.], + [ 2., 2.], + [ 42., 84.]]) + + or indices + >>> s.dropScalars([0, 1]) + >>> s.getScalarArrays() + array([[ 42., 84.]]) + + """ + if indices: + self.scalars = np.delete(self.scalars, indices, 1) + + def triangulate(self, method="areaConserving", **kwargs): + """ + Args: + method (str): triangulation method + + valid values: + - "areaConserving" + kwargs: + vicinity (list of list of int): map of point + neigbourship relations. + + Examples: + Imagine you have a camera with some pixels. + Each pixel has a center point which can be mapped to the area viewed in 3d. + >>> points = np.array([[0,0,0],[1,0,0],[2,0,0],[0,1,0],[1,1,0],[2,1,0],[3,1,0], + ... [0,2,0],[1,2,0],[1.7,1.7,0],[3,2,1],[1,3,1],[2,3,1],[3,2,1]]) + + Every pixel and thus every point has a value belonging to it. + >>> scalars = points * 42.1 + >>> sf = npTools.ScalarField3D(points, scalars=scalars) + + It is known which point belongs to which pixel + >>> pixels = np.array([[0,0],[1,0],[2,0],[0,1],[1,1],[2,1],[3,1], + ... [0,2],[1,2],[2,2],[3,2],[1,3],[2,3],[3,3]]) + + Triangulation: + >>> t = sf.triangulate(method="areaConserving", vicinity=pixels) + >>> t.getNTriangles() + 84 + >>> t[9:12] # 3rd triangle + Triangles3D([[ 1. , 1. , 0. ], + [ 1. , 0.5 , 0. ], + [ 0.66666667, 0.66666667, 0. ]]) + >>> t.triangleScalars[3] + array([ 42.1, 42.1, 0. ]) + + """ + if method == "areaConserving": + vicinity = kwargs.get('vicinity') + if vicinity is None: + raise AttributeError("kwarg vicinity is mandatory for areaConserving method.") + pointsVicinityMap = ScalarField3D(self, scalars=vicinity) + pixelTriangleCornerPoints = [] + if self.scalars.size == 0: + pixelTriangleScalars = None + else: + pixelTriangleScalars = [] + relPixels = [[1, 0], [1, -1], [0, -1]] + for i, pixel in enumerate(pointsVicinityMap.scalars): + locNeighbours = [] + for relP in relPixels: + mask = (pointsVicinityMap.scalars == pixel + relP).all(axis=1) + locNeighbours.append(mask) + + pointI = pointsVicinityMap[i] + # locNeighbours.append(locNeighbours[0]) + locNeighbours = np.array(locNeighbours) + # if not locNeighbours.sum() == 9: + # continue + for n in range(2): + if not locNeighbours[n: n + 2].any(axis=1).all(): + # not 2 neighbour points for triangle n + continue + + j = np.where(locNeighbours[n])[0][0] + k = np.where(locNeighbours[n + 1])[0][0] + + pointJ = pointsVicinityMap[j] + pointK = pointsVicinityMap[k] + + # points bisecting pointI and closePoints + halfPointIJ = 0.5 * (pointI + pointJ) + halfPointIK = 0.5 * (pointI + pointK) + halfPointJK = 0.5 * (pointJ + pointK) + + triangle = npTools.Triangles3D([pointI, pointJ, pointK]) + centroidPoint = triangle.getCentroids() + + pixelTriangleCornerPoints.extend([pointI, halfPointIJ, centroidPoint]) + pixelTriangleCornerPoints.extend([pointI, halfPointIK, centroidPoint]) + pixelTriangleCornerPoints.extend([pointJ, halfPointIJ, centroidPoint]) + pixelTriangleCornerPoints.extend([pointJ, halfPointJK, centroidPoint]) + pixelTriangleCornerPoints.extend([pointK, halfPointIK, centroidPoint]) + pixelTriangleCornerPoints.extend([pointK, halfPointJK, centroidPoint]) + + if pixelTriangleScalars is not None: + pixelTriangleScalars.extend([self.scalars[i]] * 2) + pixelTriangleScalars.extend([self.scalars[j]] * 2) + pixelTriangleScalars.extend([self.scalars[k]] * 2) + + return npTools.Triangles3D(pixelTriangleCornerPoints, + triangleScalars=pixelTriangleScalars) + + else: + raise NotImplementedError("Method {0} not implemented.".format(method)) + + def plot(self, **kwargs): + scalarIndex = kwargs.pop('scalarIndex', 0) + if not (self.scalars.size == 0 or scalarIndex is None): + kwargs['c'] = kwargs.pop('c', self.scalars.T[scalarIndex]) + return super(ScalarField3D, self).plot(**kwargs) + + +if __name__ == '__main__': + import doctest + import sympy # NOQA: F401 + doctest.testmod() diff --git a/tfields/triangles3D.py b/tfields/triangles3D.py new file mode 100644 index 0000000000000000000000000000000000000000..452c6c27ab0c63203239c5d1b05c7851df495e4d --- /dev/null +++ b/tfields/triangles3D.py @@ -0,0 +1,831 @@ +#!/usr/bin/env +# encoding: utf-8 +""" +Author: Daniel Boeckenhoff +Mail: daniel.boeckenhoff@ipp.mpg.de + +part of npTools library +""" +import numpy as np +import warnings +import npTools +import ioTools +import decoTools +import pyTools +import loggingTools + +logger = loggingTools.Logger(__name__) + + +class Triangles3D(npTools.Points3D): + # pylint: disable=R0904 + """ + Points3D child restricted to n * 3 Points. 3 Points always group together to one triangle. + Examples: + >>> t = npTools.Triangles3D([[1,2,3], [3,3,3], [0,0,0]]); t + Triangles3D([[ 1., 2., 3.], + [ 3., 3., 3.], + [ 0., 0., 0.]]) + >>> t.triangleScalars + array([], shape=(1, 0), dtype=float64) + + >>> _ = t.coordinateTransform(t.CYLINDER) + >>> a = [npTools.Points3D([[1],[2],[3]], coordSys=npTools.Points3D.CYLINDER), + ... npTools.Points3D([[3],[3],[3]]), npTools.Points3D([[5],[1],[3]])] + >>> npTools.Triangles3D(a, coordSys=npTools.Points3D.CARTESIAN) + Triangles3D([[-0.41614684, 0.90929743, 3. ], + [ 3. , 3. , 3. ], + [ 5. , 1. , 3. ]]) + + You can add triangleScalars to each triangle + >>> t.appendScalars([1]) + + """ + __slots__ = ['coordSys', 'triangleScalars', '_cache'] + + def __new__(cls, inputArray, triangleScalars=None, **kwargs): + + if any([isinstance(a, Triangles3D) for a in inputArray]) and triangleScalars is None: + if not all([isinstance(a, Triangles3D) for a in inputArray]): + # TODO: could allow if all triangleScalars are none + raise TypeError("Copy constructor of Triangles3D only" + "accepts Triangles3D instances.") + if not len(set([o.triangleScalars.shape for o in inputArray])) == 1: + raise TypeError("Shape of triangleScalars that " + "should be concatenated must be the same.") + triangleScalars = np.concatenate([o.triangleScalars for o in inputArray], axis=0) + + obj = npTools.Points3D.__new__(cls, inputArray, **kwargs) + if not obj.getNPoints() % 3 == 0: + raise TypeError("Points 3D does not describe triangles. " + "Size of obj is {0}, has no divider 3".format(len(obj))) + + # Add triangleScalars + if triangleScalars is None: + triangleScalars = np.empty((len(obj) // 3, 0)) + triangleScalars = np.array(triangleScalars) + if not len(triangleScalars) == obj.getNPoints() / 3: + raise ValueError("Length of triangleScalars has to be the same as" + "number of triangles ({0}) but is {1}." + .format(obj.getNPoints() / 3, len(triangleScalars))) + if len(triangleScalars.shape) == 1: + triangleScalars = triangleScalars.reshape(-1, 1) + obj.triangleScalars = triangleScalars + + return obj + + def saveTxt(self, filePath): + self.getMesh3D().save(filePath) + + @classmethod + def createFromTxtFile(cls, filePath): + with ioTools.TextFile(filePath, 'r') as f: + matrix = f.process(seperator=' ', lineBreak='\n') + parsedMatrix = [] + for line in matrix: + if line[0].startswith('#'): + continue + if '' in line: + pyTools.array.removeValues(line, '') + if not line: + continue + elif len(line) == 3: + parsedMatrix.append(map(float, line)) + elif len(line) == 1: + parsedMatrix.append([eval(line[0])]) + else: + raise ValueError("Length of line is not 1 or 3 but %s" % len(line)) + + infoPeriodicity = 4 + scalars = [x[0] for i, x in enumerate(parsedMatrix) if i % infoPeriodicity == 0] + pyTools.array.removePositions(parsedMatrix, + [i for i in range(len(parsedMatrix)) + if i % infoPeriodicity == 0]) + + return cls(parsedMatrix, triangleScalars=scalars) + + def convertNaN(self, value=0.): + super(Triangles3D, self).convertNaN(value) + nanIndicesScalars = np.isnan(self.triangleScalars) + self.triangleScalars[nanIndicesScalars] = value + + def appendScalars(self, scalars): + """ + Examples: + >>> t = npTools.Triangles3D([[1,2,3], [3,3,3], [0,0,0]], [[1001]]) + >>> t.appendScalars([42]) + >>> t.triangleScalars + array([[1001, 42]]) + + """ + self.triangleScalars = np.concatenate([self.triangleScalars, np.array([scalars]).T], 1) + + def getScalarArrays(self): + """ + Returns: + np.array: transposed scalar arrays + """ + return self.triangleScalars.T + + def getNTriangles(self): + """ + Returns: + int: number of triangles + """ + return self.getNPoints() // 3 + + def getMask(self, cutExpression, coordSys=None): + """ + Triangle3D implementation + """ + mask = super(Triangles3D, self).getMask(cutExpression, coordSys=coordSys) + mask = mask.reshape((self.getNTriangles(), 3)) + mask = mask.all(axis=1) + mask = np.array([mask] * 3).T.reshape((self.getNPoints())) + return mask + + def cut(self, cutExpression, coordSys=None): + """ + Default cut method for Triangles3D + Examples: + >>> x, y, z = sympy.symbols('x y z') + >>> t = npTools.Triangles3D([[1., 2., 3.], [-4., 5., 6.], [1, 2, -6], + ... [5, -5, -5], [1,0,-1], [0,1,-1], [-5, -5, -5], + ... [1,0,-1], [0,1,-1]]) + >>> tc = t.cut(x >= 0) + >>> tc + Triangles3D([[ 5., -5., -5.], + [ 1., 0., -1.], + [ 0., 1., -1.]]) + >>> tc.triangleScalars + array([], shape=(1, 0), dtype=float64) + >>> t.appendScalars([1,2,3]) + >>> tc2 = t.cut(x >= 0) + >>> tc2.triangleScalars + array([[ 2.]]) + + """ + mask = self.getMask(cutExpression, coordSys=coordSys) + + inst = self[mask].copy() + inst.triangleScalars = inst.triangleScalars[mask.reshape((self.getNTriangles(), 3)).T[0]] + + return inst + + @decoTools.cached_property() + def areas(self): + """ + Cached method to retrieve areas of triangles + """ + indices = range(self.getNTriangles()) + aIndices = [i * 3 for i in indices] + bIndices = [i * 3 + 1 for i in indices] + cIndices = [i * 3 + 2 for i in indices] + + # define 3 vectors building the triangle and take their norm + a = np.linalg.norm(self[aIndices, :] - self[bIndices, :], axis=1) + b = np.linalg.norm(self[aIndices, :] - self[cIndices, :], axis=1) + c = np.linalg.norm(self[bIndices, :] - self[cIndices, :], axis=1) + # sort by length for numerical stability + lengths = np.concatenate((a.reshape(-1, 1), b.reshape(-1, 1), c.reshape(-1, 1)), axis=1) + lengths.sort() + a, b, c = lengths.T + + return 0.25 * np.sqrt((a + (b + c)) * (c - (a - b)) * (c + (a - b)) * (a + (b - c))) + + def getAreas(self): + """ + Calculate area with "heron's formula" + Examples: + >>> m = npTools.Mesh3D([[1,0,0], [0,0,1], [0,0,0]], [[0, 1, 2]]); + >>> m.triangles.getAreas() + array([ 0.5]) + + >>> m = npTools.Mesh3D([[1,0,0], [0,1,0], [0,0,0], [0,0,1]], [[0, 1, 2], [1, 2, 3]]); + >>> m.triangles.getAreas() + array([ 0.5, 0.5]) + + >>> m = npTools.Mesh3D([[1,0,0], [0,1,0], [1,1,0], [0,0,1], [1,0,1]], + ... [[0, 1, 2], [0, 3, 4]]); + >>> m.triangles.getAreas() + array([ 0.5, 0.5]) + + """ + return self.areas + + def getAreasWithTransformation(self, transformationMatrix): + """ + Calculate area with "heron's formula". + The triangle points are transformed with transformationMatrix + """ + indices = range(self.getNTriangles()) + aIndices = [i * 3 for i in indices] + bIndices = [i * 3 + 1 for i in indices] + cIndices = [i * 3 + 2 for i in indices] + + # define 3 vectors building the triangle, transform it back and take their norm + + a = np.linalg.norm(np.linalg.solve(transformationMatrix.T, (self[aIndices, :] - self[bIndices, :]).T), axis=0) + b = np.linalg.norm(np.linalg.solve(transformationMatrix.T, (self[aIndices, :] - self[cIndices, :]).T), axis=0) + c = np.linalg.norm(np.linalg.solve(transformationMatrix.T, (self[bIndices, :] - self[cIndices, :]).T), axis=0) + + # sort by length for numerical stability + lengths = np.concatenate((a.reshape(-1, 1), b.reshape(-1, 1), c.reshape(-1, 1)), axis=1) + lengths.sort() + a, b, c = lengths.T + + return 0.25 * np.sqrt((a + (b + c)) * (c - (a - b)) * (c + (a - b)) * (a + (b - c))) + + def getCorners(self): + """ + Returns: + three np.arrays with corner points of triangles + """ + indices = range(self.getNTriangles()) + aIndices = [i * 3 for i in indices] + bIndices = [i * 3 + 1 for i in indices] + cIndices = [i * 3 + 2 for i in indices] + + a = self[aIndices, :] + b = self[bIndices, :] + c = self[cIndices, :] + return a, b, c + + def getCircumcenters(self): + """ + Semi baricentric method to calculate circumcenter points of the triangles + + Examples: + >>> m = npTools.Mesh3D([[0,0,0], [1,0,0], [-1,0,0], [0,1,0], [0,0,1]], + ... [[0, 1, 3],[0, 2, 3],[1,2,4], [1, 3, 4]]); + >>> m.triangles.getCircumcenters() + Points3D([[ 5.00000000e-01, 5.00000000e-01, 0.00000000e+00], + [ -5.00000000e-01, 5.00000000e-01, 0.00000000e+00], + [ 0.00000000e+00, 0.00000000e+00, 2.22044605e-16], + [ 3.33333333e-01, 3.33333333e-01, 3.33333333e-01]]) + + """ + pointA, pointB, pointC = self.getCorners() + a = np.linalg.norm(pointC - pointB, axis=1) # side length of side opposite to pointA + b = np.linalg.norm(pointC - pointA, axis=1) + c = np.linalg.norm(pointB - pointA, axis=1) + bary1 = a ** 2 * (b ** 2 + c ** 2 - a ** 2) + bary2 = b ** 2 * (a ** 2 + c ** 2 - b ** 2) + bary3 = c ** 2 * (a ** 2 + b ** 2 - c ** 2) + matrices = np.concatenate((pointA, pointB, pointC), axis=1).reshape(pointA.shape + (3,)) + # transpose the inner matrix + matrices = np.einsum('...ji', matrices) + vectors = np.array((bary1, bary2, bary3)).T + # matrix vector product for matrices and vectors + P = np.einsum('...ji,...i', matrices, vectors) + P /= vectors.sum(axis=1).reshape((len(vectors), 1)) + return npTools.Points3D(P) + + @decoTools.cached_property() + def centroids(self): + """ + this version is faster but takes much more ram also. + So much that i get memory error with a 32 GB RAM + """ + nT = self.getNTriangles() + mat = np.ones((1, 3)) / 3.0 + # matrix product calculatesq center of all triangles + return npTools.Points3D(np.dot(mat, self.reshape(nT, 3, 3))[0], coordSys=self.coordSys) + + """ + Old version: + pointA, pointB, pointC = self.getCorners() + return Points3D(1. / 3 * (pointA + pointB + pointC)), coordSys=self.coordSys + This versioin was slightly slower (110 % of new version) + Memory usage of new version is better for a factor of 4 or so. + Not really reliable method of measurement + """ + + def getCentroids(self): + """ + Examples: + >>> m = npTools.Mesh3D([[0,0,0], [1,0,0], [-1,0,0], [0,1,0], [0,0,1]], + ... [[0, 1, 3],[0, 2, 3],[1,2,4], [1, 3, 4]]); + >>> m.triangles.getCentroids() + Points3D([[ 0.33333333, 0.33333333, 0. ], + [-0.33333333, 0.33333333, 0. ], + [ 0. , 0. , 0.33333333], + [ 0.33333333, 0.33333333, 0.33333333]]) + + """ + return self.centroids + + def getTriangleCenters(self): + """ + Deprecated! use centroids + """ + warnings.warn("deprecated", DeprecationWarning) + return self.centroids + + def getEdgeVectors(self): + """ + Return two vectors that define two triangle edges + """ + a, b, c = self.getCorners() + + ab = b - a + ac = c - a + + return ab, ac + + def getNormVectors(self): + """ + Examples: + >>> m = npTools.Mesh3D([[0,0,0], [1,0,0], [-1,0,0], [0,1,0], [0,0,1]], + ... [[0, 1, 3],[0, 2, 3],[1,2,4], [1, 3, 4]]); + >>> m.triangles.getNormVectors() + array([[ 0. , 0. , 1. ], + [ 0. , 0. , -1. ], + [ 0. , 1. , 0. ], + [ 0.57735027, 0.57735027, 0.57735027]]) + + """ + ab, ac = self.getEdgeVectors() + vectors = np.cross(ab, ac) + norms = np.apply_along_axis(np.linalg.norm, 0, vectors.T).reshape(-1, 1) + # cross product may be zero, so replace zero norms by ones to divide vectors by norms + np.place(norms, norms == 0.0, 1.0) + return vectors / norms + + def getBaricentricCoordinates(self, point, delta=0.): + """ + Determine baricentric coordinates like + + [u,v,w] = [ab, ac, ab x ac]^-1 * ap + where ax is vector from point a to point x + + return baricentric coordinates of triangle + last coordinate is prism + Examples: + empty Meshes return right formatted array + >>> m = npTools.Mesh3D([], faces=[]) + >>> m.triangles.getBaricentricCoordinates(np.array([0.2, 0.2, 0])) + array([], dtype=float64) + + >>> m2 = npTools.Mesh3D([[0,0,0], [2,0,0], [0,2,0], [0,0,2]], [[0, 1, 2], [0, 2, 3]]); + >>> m2.triangles.getBaricentricCoordinates(np.array([0.2, 0.2, 0]), delta=2.) + array([[ 0.1, 0.1, 0. ], + [ 0.1, 0. , 0.1]]) + + if point lies in the plane, return nan, else inf for w if delta is exactly 0. + >>> m2.triangles.getBaricentricCoordinates(np.array([0.2, 0.2, 0]), delta=0.) + array([[ 0.1, 0.1, nan], + [ 0.1, 0. , inf]]) + + If you define triangles that have colinear side vectors or in general lead to + not invertable matrices [ab, ac, ab x ac] a warning wil be thrown but no error + >>> m3 = npTools.Mesh3D([[0,0,0], [2,0,0], [4,0,0], [0,1,0]], [[0, 1, 2], [0, 1, 3]]); + >>> import pytest + >>> with pytest.warns(UserWarning) as record: + ... bc = m3.triangles.getBaricentricCoordinates(np.array([0.2, 0.2, 0]), delta=0.3) + >>> bc + array([[ nan, nan, nan], + [ 0.1, 0.2, 0. ]]) + >>> len(record) + 1 + >>> "Could not invert matrix" in str(record[0].message) + True + + Returns: + arrays of u, v, w for each triangle + """ + if self.getNTriangles() == 0: + return np.array([]) + + try: + point = np.reshape(point, 3) + except ValueError: + raise ValueError("point must be castable to shape 3 but is of shape {0}" + .format(point.shape)) + except: + raise + + a, b, c = self.getCorners() + + ap = point - a + # matrix vector product for matrices and vectors + barCoords = np.einsum('...ji,...i', self.barycentricMatrix, ap) + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', message="invalid value encountered in divide") + warnings.filterwarnings('ignore', message="divide by zero encountered in divide") + barCoords[:, 2] /= delta # allow devide by 0. + return barCoords + + @decoTools.cached_property() + def barycentricMatrix(self): + """ + cached barycentric matrix for faster calculations + """ + a, b, c = self.getCorners() + ab = b - a + ac = c - a + + # get norm vector + norm = np.cross(ab, ac) + normLen = np.linalg.norm(norm, axis=1) + normLen = normLen.reshape((1,) + normLen.shape) + colinearMask = (normLen == 0) + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', category=np.VisibleDeprecationWarning) + # prevent divide by 0 + colinSlice = np.where(~colinearMask.T) + norm[np.where(~colinearMask.T)] = \ + norm[np.where(~colinearMask.T)] / normLen.T[np.where(~colinearMask.T)] + + matrix = np.concatenate((ab, ac, norm), axis=1).reshape(ab.shape + (3,)) + matrix = np.einsum('...ji', matrix) # transpose the inner matrix + + # invert matrix if possible + # matrixI = np.linalg.inv(matrix) # one line variant without exception + matrixI = [] + for mat in matrix: + try: + matrixI.append(np.linalg.inv(mat)) + except np.linalg.linalg.LinAlgError as e: + if str(e) == "Singular matrix": + warnings.warn("Singular matrix: Could not invert matrix " + "{0}. Return nan matrix." + .format(str(mat).replace('\n', ','))) + matrixI.append(np.full((3, 3), np.nan)) + return np.array(matrixI) + + def pointsInTriangles(self, points3D, delta=0., method='baryc', assignMultiple=False): + """ + Args: + points3D (Points3D instance) + + optional: + delta (float): normal distance to a triangle, that the points + is concidered to be contained in the triangle. + method (str): choose, which method to use for the calculation. + baryc is recommended + assignMultiple (bool): if True, one point may belong to multiple + triangles at the same time. In the other case the first + occurence will be True the other False + + Returns: + 2-d numpy mask which is True, where a point is in a face + face ind -> + points index + 1 0 0 + | 1 0 0 + V 0 0 1 + + if assignMultiple == False: + there is just one True for each points index + face indices can have multiple true values + + """ + log = logger.new() + log.verbose("Checking for points in Triangles") + try: + points3D = points3D.reshape((-1, 3)) + except ValueError: + raise ValueError("points3D must be castable to shape (nPoints, 3) " + "but is of shape {0}".format(points3D.shape)) + except: + raise + + if self.getNTriangles() == 0: + return np.empty((points3D.shape[0], 0), dtype=bool) + + masks = np.zeros((points3D.getNPoints(), self.getNTriangles()), dtype=bool) + for i, point in enumerate(points3D): + if i % 1000 == 0: + log.verbose("Status: {i} points processed. Yet {nP} are found " + "to be in triangles.".format(nP=masks.sum() - 1, **locals())) + masks[i] = self.pointInTriangles(point, delta, method=method) + log.verbose("Status: All points processed. Yet {nP} are found " + "to be in triangles.".format(nP=masks.sum() - 1, **locals())) + + if len(masks) == 0: + return masks + + if not assignMultiple: + """ + index of first faces containing the point for each point. This gets the + first membership index always. ignoring multiple triangle memberships + """ + faceMembershipIndices = np.argmax(masks, axis=1) + # True if point lies in any triangle + membershipBools = (masks.sum(axis=1) != 0) + + masks = np.full(masks.shape, False, dtype=bool) + for j, valid in enumerate(membershipBools): + if valid: + masks[j, faceMembershipIndices[j]] = True + """ + masks is now a matrix: + face ind -> + points index + 1 0 0 + | 1 0 0 + V 0 0 1 + + there is just one True for each points index + face indices can have multiple true values + """ + return masks + + def pointInTriangles(self, point, delta=0., method='baryc'): + """ + Returns: + np.array: boolean mask, True where point in a triangle within delta + Args: + point (list of len 3) + delta (float): acceptance in +- norm vector direction + """ + if method == 'baryc': + return self.pointInTrianglesBaryc(point, delta=delta) + elif method == 'baryc2': + return self.pointInTrianglesBarycentric2(point, delta=delta) + else: + raise NotImplementedError + + def pointInTrianglesBaryc(self, point, delta=0.): + """ + Examples: + >>> m = npTools.Mesh3D([[1,0,0], [0,1,0], [0,0,0]], [[0, 1, 2]]); + >>> m.triangles.pointInTrianglesBaryc(npTools.Points3D([[0.2, 0.2, 0]])) + array([ True], dtype=bool) + + wrong format of point will raise a ValueError + >>> m.triangles.pointInTrianglesBaryc(npTools.Points3D([[0.2, 0.2, 0], [0.2, 0.2, 0]])) + Traceback (most recent call last): + ... + ValueError: point must be castable to shape 3 but is of shape (2, 3) + + All Triangles are tested + >>> m2 = npTools.Mesh3D([[1,0,0], [0,1,0], [0,0,0], [4,0,0], [4, 4, 0], [8, 0, 0]], + ... [[0, 1, 2], [3, 4, 5]]); + + >>> m2.triangles.pointInTrianglesBaryc(np.array([0.2, 0.2, 0])) + array([ True, False], dtype=bool) + >>> m2.triangles.pointInTrianglesBaryc(np.array([5, 2, 0])) + array([False, True], dtype=bool) + + delta allows to accept points that lie within delta orthogonal to the tringle plain + >>> m2.triangles.pointInTrianglesBaryc(np.array([0.2, 0.2, 9000]), 0.0) + array([False, False], dtype=bool) + >>> m2.triangles.pointInTrianglesBaryc(np.array([0.2, 0.2, 0.1]), 0.2) + array([ True, False], dtype=bool) + + If you define triangles that have colinear side vectors or in general lead to + not invertable matrices the you will always get False + >>> m3 = npTools.Mesh3D([[0,0,0], [2,0,0], [4,0,0], [0,1,0]], [[0, 1, 2], [0, 1, 3]]); + >>> import pytest + >>> with pytest.warns(UserWarning) as record: + ... mask = m3.triangles.pointInTrianglesBaryc(np.array([0.2, 0.2, 0]), delta=0.3) + >>> mask + array([False, True], dtype=bool) + + """ + if self.getNTriangles() == 0: + return np.array([], dtype=bool) + + u, v, w = self.getBaricentricCoordinates(point, delta=delta).T + + if delta == 0.: + w[np.isnan(w)] = 0. # division by 0 in getBaricentricCoordinates makes w = 0 nan. + + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', message="invalid value encountered in less_equal") + orthogonalAcceptance = (abs(w) <= 1) + barycentricBools = ((v <= 1.) & (v >= 0.)) & ((u <= 1.) & (u >= 0.)) & ((v + u <= 1.0)) + return np.array(barycentricBools & orthogonalAcceptance) + + def pointInTrianglesBarycentric2(self, point, delta=0.): + """ + Check whether point lies within triangles with Barycentric Technique + It could lie within multiple triangles! + Examples: + >>> m = npTools.Mesh3D([[1,0,0], [0,1,0], [0,0,0]], [[0, 1, 2]]); + >>> m.triangles.pointInTrianglesBarycentric2(npTools.Points3D([[0.2, 0.2, 0]])) + array([ True], dtype=bool) + + wrong format of point will raise a ValueError + >>> m.triangles.pointInTrianglesBarycentric2(npTools.Points3D([[0.2, 0.2, 0], + ... [0.2, 0.2, 0]])) + Traceback (most recent call last): + ... + ValueError: point must be castable to shape 3 but is of shape (2, 3) + + All Triangles are tested + >>> m2 = npTools.Mesh3D([[1,0,0], [0,1,0], [0,0,0], [4,0,0], + ... [4, 4, 0], [8, 0, 0]], + ... [[0, 1, 2], [3, 4, 5]]); + >>> m2.triangles.pointInTrianglesBarycentric2(np.array([0.2, 0.2, 0])) + array([ True, False], dtype=bool) + >>> m2.triangles.pointInTrianglesBarycentric2(np.array([5, 2, 0])) + array([False, True], dtype=bool) + + delta allows to accept points that lie within delta orthogonal to the tringle plain + >>> m2.triangles.pointInTrianglesBarycentric2(np.array([0.2, 0.2, 9000]), 0.0) + array([False, False], dtype=bool) + >>> m2.triangles.pointInTrianglesBarycentric2(np.array([0.2, 0.2, 0.1]), 0.2) + array([ True, False], dtype=bool) + + Returns: + array of bools for each triangle + """ + try: + point = np.reshape(point, 3) + except ValueError: + raise ValueError("point must be castable to shape 3 but is of shape {0}" + .format(point.shape)) + except: + raise + + indices = range(self.getNTriangles()) + aIndices = [i * 3 for i in indices] + bIndices = [i * 3 + 1 for i in indices] + cIndices = [i * 3 + 2 for i in indices] + + # compute base vectors + AP = (self[aIndices, :] - point).T + AB = (self[aIndices, :] - self[bIndices, :]).T + AC = (self[aIndices, :] - self[cIndices, :]).T + + # compute barycentric coordinates + np.seterr(divide='ignore', invalid='ignore') + + denom = AB[1] * AC[0] - AB[0] * AC[1] + v = (AB[1] * AP[0] - AB[0] * AP[1]) / denom + u = -(AC[1] * AP[0] - AC[0] * AP[1]) / denom + # u = (AP[0] - v * AC[0]) / AB[0] # also true, but problematic with AB[0] + + # calculate bools + orthogonalAcceptance = abs(AP[2] - u * AB[2] - v * AC[2]) <= delta + barycentricBools = ((v <= 1.) & (v >= 0.)) & ((u <= 1.) & (u >= 0.)) & ((v + u <= 1.0)) + np.seterr(divide='warn', invalid='warn') + return np.array(barycentricBools & orthogonalAcceptance) + + def pointOnTriangleSide(self, point): + """ + Returns: + np.array: boolean mask which is true, if point is on one side ray + of a triangle + Examples: + >>> m = npTools.Mesh3D([[0,0,0], [1,0,0], [-1,0,0], [0,1,0], [0,0,1]], + ... [[0, 1, 3],[0, 2, 3],[1,2,4]]); + + Corner points are found + >>> m.triangles.pointOnTriangleSide(npTools.Points3D([[0,1,0]])) + array([ True, True, False], dtype=bool) + + Side points are found, too + >>> m.triangles.pointOnTriangleSide(npTools.Points3D([[0.5,0,0.5]])) + array([False, False, True], dtype=bool) + + """ + u, v, w = self.getBaricentricCoordinates(point, 1.).T + + orthogonalAcceptance = (w == 0) # point should lie in triangle + barycentricBools = (((0. <= v) & (v <= 1.)) & (u == 0.)) | \ + (((0. <= u) & (u <= 1.)) & (v == 0.)) | \ + (v + u == 1.0) + return np.array(barycentricBools & orthogonalAcceptance) + + def getWeightedAreas(self, weightScalarIndex=0): + """ + Args: + weightScalarIndex (int): index to scalars to weight with + """ + # set weights to 1.0 if weightScalarIndex is None + if weightScalarIndex is None: + weights = np.ones(self.getNTriangles()) + elif self.getScalarArrays().shape[0] == 0: + weights = np.ones(self.getNTriangles()) + else: + weights = self.getScalarArrays()[weightScalarIndex] + areas = self.getAreas() + return areas * weights + + def getNormedWeightedAreas(self, weightScalarIndex=0, transform=None): + """ + Args: + weightScalarIndex (int): index to scalars to weight with + Returns: + A_i * w_i / sum_{i=0}^{N}(A_i * w_i) + """ + # set weights to 1.0 if weightScalarIndex is None + if transform is None: + transform = np.eye(3) + if weightScalarIndex is None: + weights = np.ones(self.getNTriangles()) + elif self.getScalarArrays().shape[0] == 0: + weights = np.ones(self.getNTriangles()) + else: + weights = self.getScalarArrays()[weightScalarIndex] + areas = self.getAreasWithTransformation(transform) + return areas * weights / np.dot(areas, weights) + + def getMean(self, weightScalarIndex=0): + """ + Args: + weightScalarIndex (int): index to scalars to weight with + Returns: + mean of the centroids weighted by scalars @ <weightScalarIndex> + """ + centroids = self.getCentroids() + weightedAreas = self.getNormedWeightedAreas(weightScalarIndex=weightScalarIndex) + + # sum up all triangle centers weighted by the area and divided by the total area + return np.dot(centroids.T, weightedAreas) + + def getStd(self, mean=None, weightScalarIndex=0): + """ + Args: + mean(Points3D) + weightScalarIndex (int): index to scalars to weight with + """ + if mean is None: + mean = self.getMean(weightScalarIndex=weightScalarIndex) + centroids = self.getCentroids() + weightedAreas = self.getNormedWeightedAreas(weightScalarIndex=weightScalarIndex) + squaredDiff = (centroids - mean) ** 2 + + var = np.dot(squaredDiff.T, weightedAreas) + return np.sqrt(var) + + def getDirection(self, weightScalarIndex=0, transform=None): + """ + Returns: Eigenvector corresponding to greatest eigenvalue multiplied by greatest eigenvalue + """ + # calculate covariance matrix + log = logger.new() + try: + cov = np.cov(self.getCentroids().T, + ddof=0, + aweights=self.getNormedWeightedAreas(weightScalarIndex=weightScalarIndex, transform=transform)) + # calculate eigenvalues and eigenvectors of covariance + + evals, evecs = np.linalg.eigh(cov) + except: + log.warning("np.linalg.eig() failed. use zeros instead!") + evals = np.zeros(3) + evecs = np.zeros((3, 3)) + idx = np.argmax(evals) + # return eigenvector corresponding to greatest eigenvalue multiplied by greatest eigenvalue + # always positive in x-direction + return evals.max() * (evecs[:, idx] if evecs[0, idx] > 0 else -evecs[:, idx]) + + def getCovEig(self, weightScalarIndex=0): + """ + Args: + weightScalarIndex (int): index to scalars to weight with + """ + cov = np.cov(self.getCentroids().T, + ddof=0, + aweights=self.getNormedWeightedAreas(weightScalarIndex=weightScalarIndex)) + # calculate eigenvalues and eigenvectors of covariance + evals, evecs = np.linalg.eigh(cov) + idx = evals.argsort()[::-1] + evals = evals[idx] + evecs = evecs[:, idx] + e = np.concatenate((evecs, evals.reshape(1, 3))) + return e.T.reshape(12, ) + + def getMainAxes(self): + """ + Args: + weightScalarIndex (int): index to scalars to weight with + Returns: + Main Axes eigen-vectors + """ + mean = self.getMean(None) + relativeCentroids = self.getCentroids() - mean + x = relativeCentroids + cov = np.cov(x.T, + ddof=0, + aweights=self.getNormedWeightedAreas(weightScalarIndex=None)) + # calculate eigenvalues and eigenvectors of covariance + evals, evecs = np.linalg.eigh(cov) + return (evecs * evals.T).T + + def getMesh3D(self): + """ + Forward to an alternative representation of the triangeles as mesh + """ + return npTools.Mesh3D(self, + [[3 * i, 3 * i + 1, 3 * i + 2] + for i in range(self.getNTriangles())], + faceScalars=self.triangleScalars) + + def plot(self, **kwargs): + """ + Forward to Mesh3D plot + """ + return self.getMesh3D().plot(**kwargs) + + +if __name__ == '__main__': + import doctest + import sympy # NOQA: F401 + + doctest.testmod() diff --git a/tfields/vectorField3D.py b/tfields/vectorField3D.py new file mode 100644 index 0000000000000000000000000000000000000000..468ee5278f6599313728e899fb1f436936fd5b05 --- /dev/null +++ b/tfields/vectorField3D.py @@ -0,0 +1,160 @@ +#!/usr/bin/env +# encoding: utf-8 +""" +Author: Daniel Boeckenhoff +Mail: daniel.boeckenhoff@ipp.mpg.de + +part of npTools library +""" +import numpy as np +import npTools +import mplTools as mpt +import loggingTools +logger = loggingTools.Logger(__name__) + + +class VectorField3D(object): + """ + Container class for two points3D instances points and vectors + Attributes: + points + vectors + """ + def __init__(self, points, vectors, **kwargs): + """ + Examples: + >>> p = npTools.Points3D([[1, 1],[2, 2],[3, 3]], coordSys=npTools.Points3D.CYLINDER) + >>> v = npTools.Points3D([[4, 3],[5, 3],[6, 3]]) + >>> vf = npTools.VectorField3D(p, v, coordSys=npTools.Points3D.CARTESIAN) + >>> vf + points: [[-0.41614684 0.90929743 3. ] + [-0.41614684 0.90929743 3. ]], vectors: [[ 4. 5. 6.] + [ 3. 3. 3.]] + >>> vf2 = npTools.VectorField3D(p, v, coordSys=npTools.Points3D.CYLINDER) + >>> vf2 == vf + True + + """ + coordSys = kwargs.pop('coordSys', None) + if not isinstance(points, npTools.Points3D): + self.points = npTools.Points3D(points, coordSys=coordSys) + else: + self.points = points.copy() + if coordSys is None: + coordSys = points.getCoordSys() + if not isinstance(vectors, npTools.Points3D): + self.vectors = npTools.Points3D(vectors, coordSys=coordSys) + else: + self.vectors = vectors.copy() + if coordSys is None: + coordSys = vectors.getCoordSys() + if coordSys is not None: # meaning a coordinate system has been specified + self.coordinateTransform(coordSys) + if not len(self.vectors) == len(self.points): + raise ValueError("Vectors and points must be of same dimension!") + + @classmethod + def createTest(cls): + """ + A test factory method + """ + p = npTools.MeshGrid3D(3, 4, 2, 0, 2, 3, 6, 9, 4, iterOrder=[1, 0, 2], + coordSys=npTools.Points3D.CYLINDER) + v = npTools.MeshGrid3D(3, 4, 2, 0, 2, 3, 6, 9, 4, iterOrder=[1, 0, 2], + coordSys=npTools.Points3D.CYLINDER) + return cls(p, v) + + def coordinateTransform(self, coordSys): + """ + Args: + coordSys (str): see Points3D. + """ + self.points.coordinateTransform(coordSys) + self.vectors.coordinateTransform(coordSys) + + def mirrorCoordinate(self, coordinate, condition=None): + """ + Args: + coordSys (str): see Points3D. + """ + if condition is not None: + raise NotImplementedError("Mirror with condition not defined in vectorfield") + self.points.mirrorCoordinate(coordinate) + self.vectors.mirrorCoordinate(coordinate) + + def changeIterOrder(self, iterOrder): + """ + Set a new iterorder + """ + if not issubclass(self.points, npTools.MeshGrid3D): + raise TypeError("Can only change iterOrder for MeshGrid3D") + self.points.changeIterOrder(iterOrder) + self.vectors.changeGridIterOrder(iterOrder, *self.points.baseLengths, + actualIterOrder=self.points.iterOrder) + + def getCoordSys(self): + """ + Returns: + str: coordSys + """ + return self.points.getCoordSys() + + def copy(self): + """ + Returns: + copy of object + """ + return self.__class__(self.points, self.vectors) + + def __getitem__(self, index): + return self.points[index], self.vectors[index] + + def __repr__(self): + return "points: %s, vectors: %s" % (self.points, self.vectors) + + def __eq__(self, other): + if self.getCoordSys() != other.getCoordSys(): + other = other.copy() + other.coordinateTransform(self.getCoordSys()) + return bool(np.all(np.isclose(self.points, other.points)) and + np.all(np.isclose(self.vectors, other.vectors))) + + def getVectors(self): + """ + Returns: + vectors + """ + return self.vectors.copy() + + def getPoints(self): + """ + Returns: + point bases + """ + return self.points.copy() + + def evalAtPoint(self, point): + """ + Examples: + >>> vf = npTools.VectorField3D.createTest() + >>> vf.evalAtPoint(npTools.Points3D([[3, 0, 6]])) + MeshGrid3D([[ 3., 0., 6.]]) + >>> vf.evalAtPoint([3, 1, 6]) + MeshGrid3D([[ 3., 1., 6.]]) + >>> vf.evalAtPoint([[0, 0, 0]]) + MeshGrid3D([], shape=(0, 3), dtype=float64) + + """ + return self.vectors[np.equal(self.points, point).all(axis=1)] + + def plot(self, **kwargs): + """ + Frowarding to plotTools.plotVectorField + """ + artist = mpt.plotVectorField(self.points, self.vectors, **kwargs) + return artist + + +if __name__ == '__main__': + import doctest + doctest.testmod()