Source code for s3dlib.surface

# Copyright (C) Frank Zaverl, Jr.
# See file LICENSE for license information.

from s3dlib import __version__

import math
import warnings
import copy
from functools import reduce
#from time import time

import numpy as np
from scipy import interpolate, spatial
from skimage import measure

import matplotlib as mpl
from matplotlib import cm, colors, image, tri, rcParams
from mpl_toolkits.mplot3d.art3d import Poly3DCollection, Line3DCollection

#.. simple warning string
warnings.formatwarning = lambda m,c,n,l,line=0 : str(m)+'\n'

_MAXREZ = 8          # in subclass surfaces, maximum recursive triangulation.
_MAXPLREZ = 10       # in ParametricLine lines, maximum recursive segmentation.
_MINRAD = 0.01       # in subclassed polar and spherical surface split basetypes.
_SPLITSIZE = 0.0001  # for split grid geometries, phi offset from 2*pi
_MAXIEFACE = 250     # max of multi-type polyhedron faces to calc initedges
                     # ( to avoid non-optimize edge calc on large input messhes )

_COOR_KWARGS = {     # used for clipping & contour lines and line-filled surfaces
    'coor': [ [0], [0, 'planar','xyz','p','P'],
                   [1, 'c', 'C', 'cylinder',  'cylindrical'],
                   [2, 's', 'S', 'sphere', 'spherical'],
                   [3, 'x', 'X'],
                   [4, 'y', 'Y'],
                   [5, 'z', 'Z'] ]
}

_COORSYS = { "XYZ": 0, "POLAR": 1, "CYLINDRICAL": 2, "SPHERICAL": 3, "LONGLAT": 4 }

_ILLUM = (1,0,1)  # default light direction, (cmap_normals, shade, hilite & line direction).
_DFT_VLIM =     [ -1.0, 1.0 ]       # default face color value bounds
_DFT_VERTVLIM = [  0.0, 1.0 ]       # default vector magnitude value bounds
_DFT_ALR = 0.25   # default axis length ratio, head size to vector magnitude.
_DFT_VIEW = [  30, -60 ]            # default, axes elev and azim
_DFT_CMAP = rcParams['image.cmap']  # default, colormap

# +----------------------------------------------------------+
# |  S3Dlib v_1.3.0 was developed using Matplotlib v_3.8.0   |
# +----------------------------------------------------------+

# FutDev: Future Development notes.
# MROI:   Minimum Return On coding effort Investment.
#         (the juice isn't worth the squeeze for current release)

[docs]class Surface3DCollection(Poly3DCollection): """ Base class for 3D surfaces. """ _geomType = _COORSYS['XYZ']
[docs] @staticmethod def chull(points, **kargs) : """ Surface3DCollection Convex Hull for a set of 3D points. Parameters ---------- points : N x 3 float array-like Returns ------- Surface3DCollection object """ points = np.array(points) hull = spatial.ConvexHull(points) verts = points[hull.vertices] cPnt = verts.mean(axis=0) # set the faceIndices referencing the verts, not the points.... revlkup = { val:i for i,val in enumerate(hull.vertices) } fIndices = [None]*len( hull.simplices ) for i,indx in enumerate(hull.simplices): a,b,c = indx fIndices[i] = [ revlkup[a] , revlkup[b] , revlkup[c] ] # set the face indices using the RHR for outward normals from the center. temp = [None]*len(fIndices) for i,findx in enumerate(fIndices) : a,b,c = findx fcenter = (verts[a] + verts[b] + verts[c])/3 nordir = np.cross(verts[b]-verts[a],verts[c]-verts[b]) dotprod = np.dot(fcenter-cPnt,nordir) temp[i] = [a,b,c] if dotprod>0 else [c,b,a] fIndices = temp surface = Surface3DCollection(verts,fIndices, **kargs) return surface
@staticmethod def _get_cloud_points(N,dm) : """ Grid of xyz mesh coordinates within a domain. Note: Cloud points only calculated once for surface sets in the same domain. """ XD,YD,ZD = dm[0],dm[1],dm[2] dmT = dm.T A = dmT[1]-dmT[0] B=np.array( [ XD[0],YD[0],ZD[0] ]) Nj = (N+1)*1j xyz = np.mgrid[XD[0]:XD[1]:Nj,YD[0]:YD[1]:Nj, ZD[0]:ZD[1]:Nj ] return xyz,A,B @staticmethod def _get_VF(cloud,sval,A,B) : """ Surface vertices/face indices for an f(x,y,z) = constant. The cloud contains the scalar values. The surface of constant value, sval. A,B are specific to the cloud domain. """ M = cloud.shape[0] - 1 N = cloud.shape[1] - 1 P = cloud.shape[2] - 1 spacing = (1/M, 1/N, 1/P) level = 0.001 # FutDev: level should not be a constant. vol = cloud - sval verts, fvIndices, _, _ = measure.marching_cubes(vol, level, spacing=spacing) C = np.empty([len(verts),3]) C[:] = A verts = C*verts + B return verts,fvIndices @staticmethod def _impl_cloud_surf(operation, drez, cloud ,domain, fval, name, **kwargs) : dm = _interpret_domain(domain) # update for v_1.3.0 XD,YD,ZD = dm[0],dm[1],dm[2] dmT = dm.T A = dmT[1]-dmT[0] B=np.array( [ XD[0],YD[0],ZD[0] ]) if cloud is None : N = int( 10*drez ) Nj = (N+1)*1j xyz = np.mgrid[XD[0]:XD[1]:Nj,YD[0]:YD[1]:Nj, ZD[0]:ZD[1]:Nj ] cloud = operation(xyz) verts, faces = Surface3DCollection._get_VF(cloud,fval,A,B) surface = Surface3DCollection( verts, faces, **kwargs) if name is not None: surface._geomName = name return surface @staticmethod def _impl_cloud_surfset(operation, drez, cloud ,domain, numb, name, **kwargs) : dm = _interpret_domain(domain) # update for v_1.3.0 XD,YD,ZD = dm[0],dm[1],dm[2] dmT = dm.T A = dmT[1]-dmT[0] B=np.array( [ XD[0],YD[0],ZD[0] ]) if cloud is None : N = int( 10*drez ) Nj = (N+1)*1j xyz = np.mgrid[XD[0]:XD[1]:Nj,YD[0]:YD[1]:Nj, ZD[0]:ZD[1]:Nj ] cloud = operation(xyz) # extract cmap or color from kwargs. kargDft = { 'color':None, 'cmap':_DFT_CMAP } KW = KWprocessor(kargDft,'implsurfSet',**kwargs) userColor = KW.getVal('color') useDefColor = userColor is not None # use color arg if color is defined. cmapobj = KW.getVal('cmap') kwargs = KW.filter() if isinstance(cmapobj,str) : cmapobj = mpl.colormaps[cmapobj] # update for v_1.2.0 delta = 1/(numb + 1) minVal, maxVal = np.min(cloud), np.max(cloud) rngVal = maxVal - minVal surface = None for i in np.linspace(delta, 1, numb, endpoint=False) : fval = minVal + i*rngVal verts, faces = Surface3DCollection._get_VF(cloud,fval,A,B) color = userColor if useDefColor else cmapobj(i) surf = Surface3DCollection( verts, faces, color=color, **kwargs) surface = surf if surface is None else surface + surf # following for a colorbar view surface.set_cmap(cmapobj) # note: min/max is cloud values, not surface range. surface.bounds['vlim'] = [ minVal, maxVal ] if name is not None: surface._geomName = name # note: for this special case: if name is not None: surface.cname = name # cname = name return surface
[docs] @staticmethod def implsurf(operation, drez=2.0, domain=1, fval=0.0, name=None, **kwargs) : """ Surface3DCollection defined by an implicit function f(x,y,z). Parameters ---------- operation : function object Implicit function that takes one argument, a 3xN Numpy array of x,y,z coordinates. The function returns a single scalar value. The value of zero defines the implicit surface. drez : Float, optional, default: 2.0 Multiplier for the number of subdivisions within the domain. Frez values range from 1 to 10. Function evaluation will be the order of frez cubed. domain : a number, list or array, default: 1 The domain of the function evaluation. For a number, n, the x,y,z axes domains will be [-n,n]. For a 1-dimensional 2-element list, [a,b] will be assigned the domain for all 3 axes. Using a list of list (array), as [ [a,b],[c,d],[e,f] ], the domain is assigned individually for each of the three coordinate axes. fval : a number, default: 0.0 Scalar value of the function at the surface. name : string, optional, default: None. Descriptive identifier for the geometry. Returns ------- Surface3DCollection object """ if not isinstance(drez, (int,float)) : raise ValueError("Incorrect drez argument datatype passed to impf:", drez) if drez<1 or drez>10 : raise ValueError("Error: incorrect value drez passed to impf:", drez) surface = Surface3DCollection._impl_cloud_surf \ (operation, drez,None,domain,fval,name,**kwargs) if name is None: name = _getFunctionName(operation,name) if name is not None: surface._geomName = name return surface
[docs] @staticmethod def cloudsurf(cloud, domain=1, fval=0.0, name=None, **kwargs) : """ Surface3DCollection defined for a constant value in a point cloud. Parameters ---------- cloud : a (M,N,P) shaped array of point cloud values. domain : a number, list or array, default: 1 The domain of the function evaluation. For a number, n, the x,y,z axes domains will be [-n,n]. For a 1-dimensional 2-element list, [a,b] will be assigned the domain for all 3 axes. Using a list of list (array), as [ [a,b],[c,d],[e,f] ], the domain is assigned individually for each of the three coordinate axes. fval : a number, default: 0.0 Scalar value of surface contour within the cloude. name : string, optional, default: None. Descriptive identifier for the geometry. Returns ------- Surface3DCollection object """ surface = Surface3DCollection._impl_cloud_surf \ (None,None,cloud,domain,fval,name,**kwargs) return surface
[docs] @staticmethod def implsurfSet(operation, drez=2.0, domain=1, numb=2, name=None, **kwargs) : """ Set of surface contours implicit function f(x,y,z) within a domain Parameters ---------- operation : function object Implicit function that takes one argument, a 3xN Numpy array of x,y,z coordinates. The function returns a single scalar value. The value of zero defines the implicit surface. drez : Float, optional, default: 2.0 Multiplier for the number of subdivisions within the domain. Frez values range from 1 to 10. Function evaluation will be the order of frez cubed. domain : a number, list or array, default: 1 The domain of the function evaluation. For a number, n, the x,y,z axes domains will be [-n,n]. For a 1-dimensional 2-element list, [a,b] will be assigned the domain for all 3 axes. Using a list of list (array), as [ [a,b],[c,d],[e,f] ], the domain is assigned individually for each of the three coordinate axes. numb : an integer, default: 2 Number of surface contours within the domain. name : string, optional, default: None. Descriptive identifier for the geometry. Returns ------- Surface3DCollection object """ if not isinstance(drez, (int,float)) : raise ValueError("Incorrect drez argument datatype passed to impf:", drez) if drez<1 or drez>10 : raise ValueError("Error: incorrect value drez passed to impf:", drez) surface = Surface3DCollection._impl_cloud_surfset \ (operation, drez,None,domain,numb,name,**kwargs) if name is None: name = _getFunctionName(operation,name) if name is not None: surface._geomName = name return surface
[docs] @staticmethod def cloudsurfSet(cloud, domain=1, numb=2, name=None, **kwargs) : """ Surface3DCollection defined for a constant value in a point cloud. Parameters ---------- cloud : a (M,N,P) shaped array of point cloud values. domain : a number, list or array, default: 1 The domain of the function evaluation. For a number, n, the x,y,z axes domains will be [-n,n]. For a 1-dimensional 2-element list, [a,b] will be assigned the domain for all 3 axes. Using a list of list (array), as [ [a,b],[c,d],[e,f] ], the domain is assigned individually for each of the three coordinate axes. numb : an integer, default: 2 Number of surface contours within the domain. name : string, optional, default: None. Descriptive identifier for the geometry. Returns ------- Surface3DCollection object """ surface = Surface3DCollection._impl_cloud_surfset \ (None,None,cloud,domain,numb,name,**kwargs) return surface
@staticmethod def _init_triangulateBase(rez,baseVcoor,baseFaceVertexIndices, midVectFunc) : """ Recursively subdivide triangles. Each recursion subdivides each triangle by four. Parameters ---------- rez : integer, optional, default: 0 Number of recursive subdivisions of the triangulated base faces. Rez values range from 0 to 7. baseVcoor : V x 3 float array An array of 'v' number of xyz vertex coordinates. baseFaceVertextIndices : F x 3 int array An array of 'F' number of face vertex indices. midVectFunc : function object A function that takes two xyz coordinate (list of 3) representing a surface face edge. Returns one xyz coordinate at the bisection of the edge, mapped on the surface. Returns ------- indexObj : a dictionary of vertex indices, for a surface of F faces, E edges and V vertices. 'face' : F x 3 int array 'edge' : E x 2 int array vertCoor : V x 3 float array An array of V number of xyz vertex coordinates. """ # triangulateBase to _triangulateBase, update for v_1.3.0 # _triangulateBase is now a instance method. # _init_triangulateBase only used for init of derived classes. # FutDev : use single method. vCoor = baseVcoor.copy() fvIndices = [] evIndices = [] #..................................... def getVerticesLine(degree, leftIndex, rightIndex) : def recurs_edgeCoor(degree, indexOrigin, isRight, leftIndex, rightIndex, Eindex) : m = degree - 1 delta = int(np.power(2,m)) if isRight : delta = -delta i = indexOrigin - delta mid_coor = midVectFunc( vCoor[leftIndex], vCoor[rightIndex] ) currentCoorIndex = len(vCoor) vCoor.append(mid_coor) Eindex[i] = currentCoorIndex if m <= 0 : evIndices.append( [leftIndex, currentCoorIndex] ) evIndices.append( [currentCoorIndex, rightIndex] ) return recurs_edgeCoor(m,i,False,leftIndex,currentCoorIndex, Eindex) recurs_edgeCoor(m,i,True,currentCoorIndex,rightIndex, Eindex) return #..................................... iOrigin = int(np.power(2,degree)) Eindex = [None]*(iOrigin+1) Eindex[0] = leftIndex Eindex[iOrigin] = rightIndex if degree==0 : evIndices.append( [leftIndex, rightIndex] ) return Eindex recurs_edgeCoor(degree, iOrigin, False, leftIndex, rightIndex, Eindex) return Eindex #..................................... def recurs_faceIndices(A,B,C,rez,atCenter=True): if rez==0 : abc = [ A[0], B[0], C[0] ] fvIndices.append( abc ) if atCenter : evIndices.append( [ B[0], C[0] ] ) return # ----------------------------------------------------- J = int( (len(A)-1)/2 ) K = J + 1 # construct the center sub triangle edge vertices.... if rez==1 : xz = [ A[1], C[1] ] yx = [ B[1], A[1] ] zy = [ C[1], B[1] ] else: xz = getVerticesLine(rez-1, A[J], C[J] ) yx = getVerticesLine(rez-1, B[J], A[J] ) zy = getVerticesLine(rez-1, C[J], B[J] ) # construct the 4 sub triangles... recurs_faceIndices(A[:K],xz,C[J:],rez-1) recurs_faceIndices(B[:K],yx,A[J:],rez-1) recurs_faceIndices(C[:K],zy,B[J:],rez-1) recurs_faceIndices(yx[::-1],zy[::-1],xz[::-1],rez-1,False) return #..................................... # generate the base edge indices for each face baseFaceEdgeIndices = [] edgeArrayIndexMatrix = [[None for x in range(len(vCoor))] for y in range(len(vCoor))] for face in baseFaceVertexIndices : A = [ face[0],face[1]] B = [ face[1],face[2]] C = [ face[2],face[0]] edgeIndices = [A,B,C] baseFaceEdgeIndices.append(edgeIndices) for vertexIndex in edgeIndices : i = vertexIndex[0] j = vertexIndex[1] if edgeArrayIndexMatrix[i][j] is None: edgeArrayIndexMatrix[i][j] = getVerticesLine(rez, i, j ) edgeArrayIndexMatrix[j][i] = edgeArrayIndexMatrix[i][j].copy()[::-1] # trianglulate the base triangles for face in baseFaceEdgeIndices : A = edgeArrayIndexMatrix[face[0][0]][face[0][1]] B = edgeArrayIndexMatrix[face[1][0]][face[1][1]] C = edgeArrayIndexMatrix[face[2][0]][face[2][1]] x = (rez > 0) recurs_faceIndices(A,B,C,rez, x) vertexCoor = np.array(vCoor) indexObj = { 'face': np.array(fvIndices) , 'edge': np.array(evIndices) } return indexObj ,vertexCoor @staticmethod def _rectangulateBase(rez,baseVcoor,baseFaceVertexIndices, midVectFunc) : """ Recursively subdivide quadrilaterals. Each recursion subdivides each quadrilateral by four. Parameters ---------- rez : integer, optional, default: 0 Number of recursive subdivisions of the rectangle base faces. Rez values range from 0 to 7. baseVcoor : V x 3 float array An array of 'v' number of xyz vertex coordinates. baseFaceVertextIndices : F x 4 int array An array of 'F' number of face vertex indices. midVectFunc : function object A function that takes two xyz coordinate (list of 4) representing a surface face edge. Returns one xyz coordinate at the bisection of the edge, mapped on the surface. Returns ------- indexObj : a dictionary of vertex indices, for a surface of F faces, E edges and V vertices. 'face' : F x 4 int array 'edge' : E x 2 int array vertCoor : V x 3 float array An array of V number of xyz vertex coordinates. """ # rectangulateBase to _rectangulateBase, update for v_1.3.0 vCoor = baseVcoor.copy() fvIndices = [] evIndices = [] #..................................... def getVerticesLine(degree, leftIndex, rightIndex) : def recurs_edgeCoor(degree, indexOrigin, isRight, leftIndex, rightIndex, Eindex) : m = degree - 1 delta = int(np.power(2,m)) if isRight : delta = -delta i = indexOrigin - delta mid_coor = midVectFunc( vCoor[leftIndex], vCoor[rightIndex] ) currentCoorIndex = len(vCoor) vCoor.append(mid_coor) Eindex[i] = currentCoorIndex if m <= 0 : evIndices.append( [leftIndex, currentCoorIndex] ) evIndices.append( [currentCoorIndex, rightIndex] ) return recurs_edgeCoor(m,i,False,leftIndex,currentCoorIndex, Eindex) recurs_edgeCoor(m,i,True,currentCoorIndex,rightIndex, Eindex) return #..................................... iOrigin = int(np.power(2,degree)) Eindex = [None]*(iOrigin+1) Eindex[0] = leftIndex Eindex[iOrigin] = rightIndex if degree==0 : evIndices.append( [leftIndex, rightIndex] ) return Eindex recurs_edgeCoor(degree, iOrigin, False, leftIndex, rightIndex, Eindex) return Eindex #..................................... def recurs_faceIndices(A,B,C,D,rez): # similar to inner function of triangulateBase if rez==0 : abc = [ A[0], B[0], C[0], D[0] ] fvIndices.append( abc ) return # ----------------------------------------------------- J = int( (len(A)-1)/2 ) K = J + 1 # midpoint indices ............... mid_coor = midVectFunc( vCoor[ D[J] ], vCoor[ B[J] ] ) i_E = len(vCoor) vCoor.append(mid_coor) # arrays of the four inner quadrilaterals .............. pE = getVerticesLine(rez-1, A[J], i_E ) qE = getVerticesLine(rez-1, B[J], i_E ) rE = getVerticesLine(rez-1, C[J], i_E ) sE = getVerticesLine(rez-1, D[J], i_E ) # sudivide the four inner quadraterals ................. recurs_faceIndices( A[:K], pE, sE[::-1], D[J:], rez-1) recurs_faceIndices( B[:K], qE, pE[::-1], A[J:], rez-1) recurs_faceIndices( C[:K], rE, qE[::-1], B[J:], rez-1) recurs_faceIndices( D[:K], sE, rE[::-1], C[J:], rez-1) return #..................................... # generate the base edge indices for each face baseFaceEdgeIndices = [] edgeArrayIndexMatrix = [[None for x in range(len(vCoor))] for y in range(len(vCoor))] for face in baseFaceVertexIndices : A = [ face[0],face[1]] B = [ face[1],face[2]] C = [ face[2],face[3]] D = [ face[3],face[0]] edgeIndices = [A,B,C,D] baseFaceEdgeIndices.append(edgeIndices) for vertexIndex in edgeIndices : i = vertexIndex[0] j = vertexIndex[1] if edgeArrayIndexMatrix[i][j] is None: edgeArrayIndexMatrix[i][j] = getVerticesLine(rez, i, j ) edgeArrayIndexMatrix[j][i] = edgeArrayIndexMatrix[i][j].copy()[::-1] # rectangulate the base quadrilaterals for face in baseFaceEdgeIndices : A = edgeArrayIndexMatrix[face[0][0]][face[0][1]] B = edgeArrayIndexMatrix[face[1][0]][face[1][1]] C = edgeArrayIndexMatrix[face[2][0]][face[2][1]] D = edgeArrayIndexMatrix[face[3][0]][face[3][1]] recurs_faceIndices(A,B,C,D,rez) vertexCoor = np.array(vCoor) indexObj = { 'face': np.array(fvIndices) , 'edge': np.array(evIndices) } return indexObj ,vertexCoor
[docs] @staticmethod def coor_convert(xyz, tocart=False) : """Coordinate transformation. To be overriddden by any subclass not in Cartesian coordinates. """ xyz = np.array(xyz) return xyz
@staticmethod def _map3Dto2Dsquare(xyz) : """ Map surface to rectangular unit coordinates (0,1). """ x,y,_ = xyz a = (x+1)/2 b = (y+1)/2 return [a,b] @staticmethod def _sfev(rez,fevArray) : """ Calculate number of surface faces, edges, vertices. """ # vestigial Fo, phi, Vx = fevArray eta = np.power(2,rez) F = eta*eta*Fo E = np.int( 3*F/2 + eta*phi ) V = np.int( F/2 + eta*phi + Vx ) return [F,E,V] @classmethod def _grid_face_indices(cls,U, V, isSplit=False, isTria=False) : """ method only called by the classmethod _square_net or the CylindricalSurface classmethod _cyl_vol_square_net """ if isSplit : t = np.linspace(0,V*(U+1)-1,V*(U+1),dtype=int ) ts = t.reshape( (V,U+1) ) a = ts[:,0:U].flatten() findx = np.array( [ a, a+1, a+U+2, a+U+1 ] ) else : a = np.linspace(0,V*U-1,V*U,dtype=int ) bs = a.reshape( (V,U) ).T order = np.append(np.linspace(1,U-1,U-1,dtype=int),0) b = bs[order].T.flatten() findx = np.array( [ a, b, b+U, a+U ] ) if isTria : a,b,c,d = findx findx = np.concatenate( ( [a,b,c],[c,d,a] ), axis=1 ) return findx.T @classmethod def _square_net(cls, numV, numU, basetype,minRad, splitSize, name, **kwargs) : """ method only called by inherited class classmethods 'grid'. """ divLimits = [1,3] if cls._geomType is _COORSYS['XYZ'] : divLimits = [1,1] if cls._geomType is _COORSYS['SPHERICAL'] : divLimits = [2,3] if numV < divLimits[0] : raise ValueError('Grid division {} must be greater or equal to {}'.format(str(numV),divLimits[0] )) if numU < divLimits[1] : raise ValueError('Grid division {} must be greater or equal to {}'.format(str(numU),divLimits[1] )) if basetype == None : basetype = 'd' if minRad is None : minRad = _MINRAD if splitSize is None : splitSize = _SPLITSIZE basetype = basetype.lower() baselist = ['d','s','q','w','r','x'] if basetype not in baselist : raise ValueError("Grid basetype '{}' is not recognized. Possible values are: {}.".format(basetype,list(baselist))) isSplit = (basetype == 's') or (basetype == 'w') or (basetype == 'x') needsTriag = not ( (basetype == 'r') or (basetype == 'x') ) hasCenter = (basetype == 'd') or (basetype == 's') # hasCenter does not apply for cylindrical grids of this type. if cls._geomType is _COORSYS['CYLINDRICAL'] : hasCenter=False # Note: numX and num_X are the number of divisions and the number of coor positions. if cls._geomType is _COORSYS['XYZ'] : isSplit = True hasCenter = False num_U,min_U, max_U = numU+1,-1.0,1.0 num_V = numV + 1 coor_U = np.linspace(min_U,max_U,num_U) U_coor = np.tile(coor_U,num_V) else : # T angle ranges, used for disk,cylinder and sphere..... num_U = numU+1 if isSplit else numU if hasCenter : num_V = numV if cls._geomType is _COORSYS['POLAR'] else numV-1 else : num_V = numV+1 min_U = 0.0 max_U = (1-1/num_U)*2*np.pi if isSplit : max_U = (2-splitSize)*np.pi coor_U = np.linspace(min_U,max_U,num_U) U_coor = np.tile(coor_U,num_V) totVerts = num_U*num_V if cls._geomType is _COORSYS['XYZ'] : val_W = 0.0 # z is constant at 0. coor_V = np.linspace(-1.0,1.0,num_V) order_abc = lambda u,v,w :[u,v,w] if cls._geomType is _COORSYS['POLAR'] : val_W = 0.0 # z is constant at 0. end_V = 1/numV if hasCenter else minRad coor_V = np.linspace(1.0,end_V,num_V) order_abc = lambda u,v,w : [v,u,w] if cls._geomType is _COORSYS['CYLINDRICAL'] : val_W = 1.0 # r is constant at 1. coor_V = np.linspace(-1.0,1.0,num_V) order_abc = lambda u,v,w : [w,u,v] if cls._geomType is _COORSYS['SPHERICAL'] : val_W = 1.0 # r is constant at 1. st_angle = np.pi/numV if hasCenter else np.arcsin(minRad) stt_V = np.pi - st_angle end_V = st_angle coor_V = np.linspace(stt_V,end_V,num_V) order_abc = lambda u,v,w : [w,u,v] W_coor = np.tile(val_W,totVerts) V_coor = np.tile(coor_V,(num_U,1)) V_coor = np.ravel(V_coor,order='F') abc = order_abc(U_coor,V_coor,W_coor) abc = np.array(abc) xyz = cls.coor_convert(abc,True) verts = np.array(xyz).T numVt = num_V-1 if hasCenter else numV faceIndices = cls._grid_face_indices(numU,numVt,isSplit,needsTriag) if hasCenter : # add top triangular faces for polar & spherical surfaces ... topVert = np.array([[0.0,0.0,val_W]]) topIndex = totVerts startIndex = topIndex - num_U endIndex = topIndex-2 if isSplit else topIndex-1 a = np.linspace(startIndex,endIndex,numU,dtype=int) b = a+1 if isSplit else np.append( a[1:], [startIndex] ) c = np.full(numU,topIndex) abc = np.array([a,b,c]).T verts = np.append(verts,topVert, axis=0) faceIndices = np.append(faceIndices,abc, axis=0) if cls._geomType is _COORSYS['SPHERICAL'] : # add bottom triangular faces for spherical surfaces ... btmVert = np.array([[0.0,0.0,-val_W]]) btmIndex = totVerts+1 startIndex,endIndex = 0,numU-1 a = np.linspace(startIndex,endIndex,numU,dtype=int) b = a+1 if isSplit else np.append( a[1:], [startIndex] ) c = np.full(numU,btmIndex) bac = np.array([b,a,c]).T # bac instead for abc for outward normals. verts = np.append(verts,btmVert, axis=0) faceIndices = np.append(faceIndices,bac, axis=0) surface = cls(**kwargs) # set to the class default object # reset verts, faceIndices and edges (retain class coordinate system). surface.vertexCoor = verts surface.fvIndices = faceIndices surface.evIndices = surface._get_edges_from_faces(faceIndices) surface.vfIndicesList = surface._vertexCommonFaceIndices() surface.efIndicesList = surface._edgeCommonFaceIndices() surface.set_verts( verts[faceIndices] ) surface._set_geometric_bounds() surface.name = name surface._basetype = 'grid_'+basetype return surface @classmethod def _fev(cls,rez,basetype,dercls) : """ Sets up _sfev method, based on calls from the subclass dercls. """ # vestigial if (rez<0) or (rez>_MAXREZ) : raise ValueError('Incorrest rez of {}, must be an int, 0 <= rez <= {}'.format(rez,_MAXREZ)) if basetype is None: basetype = dercls._default_base basesurf = dercls._base_surfaces if basetype not in basesurf : raise ValueError('Basetype {} is not recognized. Possible values are: {}'.format(basetype,list(basesurf))) return cls._sfev(rez, basesurf[basetype]['fevParam'] ) @classmethod def _uvw_to_xyz(cls,rst,abc) : """ Rotational transformation at a coordinate. To be overridden by any subclass not in Cartesian coordinates. """ return np.transpose(rst) @classmethod def _disp_to_xyz( cls, uvw, theta=None, phi=None) : """ Rotational transformation from rotation angles. """ if theta is None : return uvw # each set of uvw has a different rotation matrix # that must be evaluated one at a time. MROI if phi is None : phi = np.zeros( len(theta) ) uvwT = np.transpose(uvw) dispVect = [] for i in range(len(theta)) : rotMx = eulerRot(theta[i],phi[i], inrad=True) vec = np.dot( uvwT[i], rotMx ) dispVect.append(vec) return dispVect def _triangulateBase(self,rez,midVectFunc) : """ Recursively subdivide triangles. Each recursion subdivides each triangle by four. Includes updated calculations for vertex normals and, if vertex values are not None, vertex values. Parameters ---------- rez : integer, optional, default: 0 Number of recursive subdivisions of the triangulated base faces. Rez values range from 0 to 7. midVectFunc : function object A function that takes two xyz coordinate (list of 3) representing a surface face edge. Returns one xyz coordinate at the bisection of the edge, mapped on the surface. Returns ------- indexObj : a dictionary of vertex indices, for a surface of F faces, E edges and V vertices. 'face' : F x 3 int array 'edge' : E x 2 int array vertCoor : V x 3 float array An array of V number of xyz vertex coordinates. vertexValues V float array An array of values for each vertex. """ # update 1.2.0 to 1.3.0 : # this object method replaces the class method triangulateBase. # NOTE: arrays are converted to lists (mutable) due to scope # accessability within the nested inner functions. # initialize vertex values, coordinates, phong_normals. Also # face vertex-indicies and edge vertex-indicies. vCoor = list(self.vertexCoor) pvNrm = list(self._get_vertex_normals().copy()) vVals = None if self.vertexVals is not None : vVals = list(self.vertexVals) fvIndices = [] evIndices = [] baseFaceVertexIndices = self.fvIndices #..................................... def getVerticesLine(degree, leftIndex, rightIndex) : def recurs_edgeCoor(degree, indexOrigin, isRight, leftIndex, rightIndex, Eindex) : m = degree - 1 delta = int(np.power(2,m)) if isRight : delta = -delta i = indexOrigin - delta mid_coor = midVectFunc( vCoor[leftIndex], vCoor[rightIndex] ) currentCoorIndex = len(vCoor) vCoor.append(mid_coor) # .... update for v_1.3.0 if vVals is not None : mid_val = (vVals[leftIndex] + vVals[rightIndex])/2 vVals.append(mid_val) mid_norm = pvNrm[leftIndex] + pvNrm[rightIndex] mid_norm = np.divide( mid_norm, np.linalg.norm(mid_norm) ) pvNrm.append(mid_norm) # ....................... Eindex[i] = currentCoorIndex if m <= 0 : evIndices.append( [leftIndex, currentCoorIndex] ) evIndices.append( [currentCoorIndex, rightIndex] ) return recurs_edgeCoor(m,i,False,leftIndex,currentCoorIndex, Eindex) recurs_edgeCoor(m,i,True,currentCoorIndex,rightIndex, Eindex) return #..................................... iOrigin = int(np.power(2,degree)) Eindex = [None]*(iOrigin+1) Eindex[0] = leftIndex Eindex[iOrigin] = rightIndex if degree==0 : evIndices.append( [leftIndex, rightIndex] ) return Eindex recurs_edgeCoor(degree, iOrigin, False, leftIndex, rightIndex, Eindex) return Eindex #..................................... def recurs_faceIndices(A,B,C,rez,atCenter=True): if rez==0 : abc = [ A[0], B[0], C[0] ] fvIndices.append( abc ) if atCenter : evIndices.append( [ B[0], C[0] ] ) return # ----------------------------------------------------- J = int( (len(A)-1)/2 ) K = J + 1 # construct the center sub triangle edge vertices.... if rez==1 : xz = [ A[1], C[1] ] yx = [ B[1], A[1] ] zy = [ C[1], B[1] ] else: xz = getVerticesLine(rez-1, A[J], C[J] ) yx = getVerticesLine(rez-1, B[J], A[J] ) zy = getVerticesLine(rez-1, C[J], B[J] ) # construct the 4 sub triangles... recurs_faceIndices(A[:K],xz,C[J:],rez-1) recurs_faceIndices(B[:K],yx,A[J:],rez-1) recurs_faceIndices(C[:K],zy,B[J:],rez-1) recurs_faceIndices(yx[::-1],zy[::-1],xz[::-1],rez-1,False) return #..................................... # generate the base edge indices for each face baseFaceEdgeIndices = [] edgeArrayIndexMatrix = [[None for x in range(len(vCoor))] for y in range(len(vCoor))] for face in baseFaceVertexIndices : A = [ face[0],face[1]] B = [ face[1],face[2]] C = [ face[2],face[0]] edgeIndices = [A,B,C] baseFaceEdgeIndices.append(edgeIndices) for vertexIndex in edgeIndices : i = vertexIndex[0] j = vertexIndex[1] if edgeArrayIndexMatrix[i][j] is None: edgeArrayIndexMatrix[i][j] = getVerticesLine(rez, i, j ) edgeArrayIndexMatrix[j][i] = edgeArrayIndexMatrix[i][j].copy()[::-1] # trianglulate the base triangles for face in baseFaceEdgeIndices : A = edgeArrayIndexMatrix[face[0][0]][face[0][1]] B = edgeArrayIndexMatrix[face[1][0]][face[1][1]] C = edgeArrayIndexMatrix[face[2][0]][face[2][1]] x = (rez > 0) recurs_faceIndices(A,B,C,rez, x) vertexCoor = np.array(vCoor) vertexValues = np.array(vVals) phongNorms = np.array(pvNrm) #print('[_triangulateBase] shape of vertexCoor,phongNorms',vertexCoor.shape,phongNorms.shape) indexObj = { 'face': np.array(fvIndices) , 'edge': np.array(evIndices) } return indexObj ,vertexCoor, vertexValues, phongNorms def _postProc_surfaceColors(self,onlyFaces=False) : """ Matplotlib will 'fillin' colors during rendering using the facecolor array. (this is NOT the assigned color to faces). However, S3Dlib will use the inherited _facecolors for the assigned face colors. Whenever color is 'externally' assigned or reassigned, need to directly assign colors for each face for any method that uses facecolors. This occurs initially in the method before colors are assigned (set_color, +, clipping, shading, edges, etc.) Note that when initialization using **kargs, onlyFaces should be True so don't reset edgecolors if they are assigned. """ N = len(self.fvIndices) # number of faces initFC = self._dfacecolors # update for v_1.2.0 inLen = len(initFC) # number of facecolors if inLen == N : return if isinstance(initFC,np.ndarray) : initFC = initFC.tolist() n = math.ceil(N/inLen) colorcoll = initFC*n if onlyFaces : self.set_facecolor(colorcoll[0:N]) #if onlyFaces : self._dfacecolors = colorcoll[0:N] # update for v_1.2.0 else : self.set_color(colorcoll[0:N]) return def _set_index_relations(self) : """ from the vertex and faceIndices, set the indices: evIndices: Array, for each edge, 2 vertex indicies. vfIndicesList: List, for each vertex, face indices. efIndicesList: List, for each edge, 0 to 2 face indices. """ edgeIndices = self._get_edges_from_faces(self.fvIndices) self.evIndices = np.array(edgeIndices) self.vfIndicesList = self._vertexCommonFaceIndices() self.efIndicesList = self._edgeCommonFaceIndices() return @staticmethod def _extract_vals_from_inputCoor(ponts) : """ Separate (N,4) into (N,3) and (N) arrays, or just (N,3) and None. Used by __init__ and subclassed classmethods pntsurf """ # update for v_1.3.0 tempCoor = np.array(ponts,dtype=float) vVals = None if tempCoor.shape[1] == 3 : vCoor = tempCoor else : vCoor = tempCoor[:,:3] vVals = tempCoor[:,3] return vCoor,vVals def __init__(self, vertexCoor, faceIndices, name=None, vcolor=None, **kwargs) : """ 3D surface of connected F faces, each face with N number of vertices. The surface has V number of vertices and E number of edges. Parameters ---------- vertexCoor : V x 3, or V X 4, float array-like An array of V number of xyz vertex coordinates. If V X 4, scaler values are assigned to each vertex. faceIndices : F x N int list or array A list or array of F number of faces with N vertex indices. For lists, N may be different for each face. name : string, optional, default: None. Descriptive identifier for the geometry. vcolor : V x 3 float array-like, default: None. An array of V number of vertex colors. (not implemented, reserved for future development) Other Parameters ---------------- **kwargs All other arguments are passed on to mpl_toolkits.mplot3d.art3d.Poly3DCollection. Valid argument names include: color, edgecolors, facecolors, linewidths, cmap. """ # following for a 'control limit' parameters in kwargs. KW = KWprocessor({'maxieface':_MAXIEFACE},'Surface3DCollection.__init__',False,**kwargs) maxieface = KW.getVal('maxieface') kwargs = KW.filter() dataPts = np.array(vertexCoor,dtype=float) # update for v_1.3.0 dataPts, vVal = Surface3DCollection._extract_vals_from_inputCoor(dataPts) self.vertexCoor = dataPts self.vertexVals = vVal # for possible 'smooth' triangulated face shading. # update for v_1.3.0 self.vertexNorms = None # geometric from face normals. self.phongNorms = None # linear approximate on a flat face during triangulation. self._initedges = None # assigned for irregulary type face meshes. (1.3 update) self.baseVertexCoor = np.array(self.vertexCoor,dtype=float) # update for v_1.3.0 self.fvIndices = np.array(faceIndices, dtype=object) # update for v_1.2.0 if self.fvIndices.ndim == 1 : # assume input is a list of faces with different number of edges. # the following restricts usage for non-optimized edge calc for large # meshes containing non-uniform polyhedral face types. Otherwize, only # triangulated meshes are available. if len(self.fvIndices) <= maxieface : # update for v_1.3.0 self._initedges = self._get_edges_from_faces(self.fvIndices) self._initverts = np.array(self.vertexCoor,dtype=float) # update for v_1.3.0 fvIndNew = self._triangulate_faceIndices(faceIndices) fcolor = None # note: if edgecolors are not defined, default to defined facecolor list, # not the defined color ( continue with different 'magic' ) MROI. # following 'may' be vestigial to be removed ? if 'color' in kwargs : fcolor = kwargs['color'] if 'facecolor' in kwargs : fcolor = kwargs['facecolor'] if colors.is_color_like(fcolor) : # single color assigned fvIndNew = self._triangulate_faceIndices(faceIndices) else : if fcolor is None : # no color assigned fvIndNew = self._triangulate_faceIndices(faceIndices) else : # list of colors assigned fvIndNew, faceColorsNew = self._triangulate_faceIndices_faceColors(faceIndices,fcolor) kwargs['facecolor'] = faceColorsNew kwargs['color'] = faceColorsNew self.fvIndices = np.array(fvIndNew) else : self.fvIndices = np.array(faceIndices) # update for v_1.2.0 self._set_index_relations() f = self.fvIndices v = self.vertexCoor super().__init__(v[f], **kwargs) # note: in following, only set the facecolors since Matplotlib auto-setting # the edges, if not explicitely passed with edgecolor kwargs. self._postProc_surfaceColors(True) #self._geomName = None # Redundant from next line, internal tracking to determine if set... self.name = name self.valuesName = None # ======================================================================== self.coorType = _COORSYS["XYZ"] self.baseSurfaceColor = np.array(self._dfacecolors[0]).flatten().tolist() # update for v_1.2.0 self.vertexColor = np.array(self._dfacecolors[0]).copy()[np.newaxis,:] # update for v_1.2.0 # following assigned when surface color applied using an operation self.vertexValues = None self.faceValues = None if len (self._dedgecolors) == 0 : # update for v_1.2.0 self.baseEdgeColor = self.baseSurfaceColor else : self.baseEdgeColor = np.array(self._dedgecolors[0]).flatten().tolist() # update for v_1.2.0 self.isSurfaceColored = False # flag set True for color array export. self.onlyColoredFaces = False # flag set True for cmap normals. self._surfaceShapeChanged = False # control flag for image operations self._isCompositeSurface = False # control flag for a composite object. self.disp_Vector = None # overridden in subclass. self._normal_scale = 1.0 # overridden in subclass default self.restoredClip = None # dictionary to restore from clip operation. self.vector_field = None # for vector fields when transformed. self._svd_dict = None # results from map_geom_from_svd self.scale = [1,1,1] # for axis when transformed. self.translation = [0,0,0] # for axis when transformed. self.rotation = np.identity(3) # for axis when transformed. self._rez = 0 # used by child classes. self._totRez = 0 # account for triangulation, update for v_1.3.0 self._triNumber = 0 # flag, triangulation vertex normals self._bounds = {} self._set_geometric_bounds() self._bounds['vlim'] = _DFT_VLIM self._bounds['vertvlim'] = _DFT_VERTVLIM
[docs] def __add__(self, othr) : """ Combine two surface objects into a single surface object. Parameters ---------- othr : Surface3DCollection object Returns ------- Surface3DCollection object """ if not isinstance(othr, Surface3DCollection) : raise ValueError('Add operations can only apply between Surface3DCollection objects.') # MROI: should check if 2 surface faces are both triangular or quadrilateral topVerCoor = self.vertexCoor botVerCoor = othr.vertexCoor totVerCoor = np.append(topVerCoor,botVerCoor,axis=0) nextIndex = len(topVerCoor) topFVindices = self.fvIndices botFVindices = othr.fvIndices FVtail = np.add(botFVindices,nextIndex) totFVindices = np.append(topFVindices,FVtail,axis=0) obj = Surface3DCollection(totVerCoor,totFVindices) obj._isCompositeSurface = True top_onearr = np.ones( len(topFVindices) )[:, np.newaxis] top_orig_colors = self._dfacecolors # update for v_1.2.0 if len(top_orig_colors) == 1 : top_orig_colors = top_orig_colors*top_onearr bot_onearr = np.ones( len(botFVindices) )[:, np.newaxis] bot_orig_colors = othr._dfacecolors # update for v_1.2.0 if len(bot_orig_colors) == 1 : bot_orig_colors = bot_orig_colors*bot_onearr total_colors = np.append(top_orig_colors,bot_orig_colors,axis=0) obj.set_color(total_colors) obj._set_geometric_bounds() return obj
def __str__(self) : # parent class doesn't have specific properties... # Note: self._rez may be a string or int. probably not good but MROI try: bs = ' ( {} , {} )'.format(self._rez,self._basetype) except: bs = ' ' numFaces = len(self.fvIndices) numVerts = len(self.vertexCoor) name = self.__class__.__name__ sz = ': faces: {}, vertices: {}'.format(numFaces,numVerts) val = name+bs+sz return val def _calc_rez(self,N0) : # string val for equivalent grid rez numFaces = len(self.fvIndices) cRez = np.log2(numFaces/N0)/2 return '{:.1f}'.format(cRez) def _vertexCommonFaceIndices(self) : """ list of face indices at each vertex. """ # note: a vertex may have NO faces (empty faceIndex list) # due to clipping or a vertex not used for a face, so: vfIndicesList = [ [] for i in range(len(self.vertexCoor))] # note: usually vertices may have 1 to 6 common faces for i,face in enumerate(self.fvIndices) : for vInx in face : if vfIndicesList[vInx] is None : vfIndicesList[vInx] = [i] else : vfIndicesList[vInx].append(i) return vfIndicesList def _edgeCommonFaceIndices(self) : """ list of face indices adjacent to each edge. """ efIndicesList = [None]*len(self.evIndices) # note: edges may have 1 or 2 common faces for i,eIndex in enumerate(self.evIndices) : head_vfi = self.vfIndicesList[ eIndex[0] ] tail_vfi = self.vfIndicesList[ eIndex[1] ] for fi in head_vfi : if (fi in tail_vfi) : if efIndicesList[i] is None: efIndicesList[i] = [fi] else: efIndicesList[i].append(fi) return efIndicesList def _contourLineCollection( self, dotprod, projVect, *args ) : """ ColorLine3DCollection of segments on faces interecting a plane. dotprod - dot product between each vertex and plane normal. projVect - interection plane normal *args - list of distances of the plane to the origin. """ # FutDev: There are 'vestigal' loops during early development # debugging which need to be cleaned out. (MROI for release) # --------------------------------------------------------------- def getLineVals( dotprod,aLen,projVect) : # ................................................................ def order_value( face,eIndices, conCoor, planeNormal) : A = eIndices[0] B = eIndices[1] fvi = self.fvIndices[face] verts = self.vertexCoor[ fvi ] faceNormal = self._get_face_normals([verts]) edgeVert_A = conCoor[A] edgeVert_B = conCoor[B] A_to_B = np.subtract(edgeVert_B, edgeVert_A) edgeCross = np.cross(A_to_B, planeNormal ) edgeCross= np.divide( edgeCross, np.linalg.norm(edgeCross) ) test = np.dot(faceNormal, edgeCross) return [A,B] if test>0 else [B,A] # ................................................................ # .. get intersect coor for each edge........ evi = self.evIndices verts = self.vertexCoor dp_a = aLen - dotprod eElev = dp_a[evi] # eElev is the distance of the two edge vertices above/below the plane. # edges cross the plane if one vertex is above, the other below. # hence the product of the two distances is negative if edge crosses the plane. edChk = eElev[:,0]*eElev[:,1] xCoor = [] # intersect coordinates. evix = [] # the edge index of the intersect coordinate. for i in range( len(evi) ) : if edChk[i] < 0.0 : p1 = verts[ evi[i,0] ] p2 = verts[ evi[i,1] ] dp1 = dotprod [ evi[i,0] ] dp2 = dotprod [ evi[i,1] ] a_mdp1 = dp_a [ evi[i,0] ] lmb = a_mdp1/( dp2 - dp1 ) px = lmb*np.subtract(p2,p1) + p1 xCoor.append(px) evix.append(i) # .. determine faces having edges which have an intersect..... if len(xCoor) == 0 : return None,None,None efi = self.efIndicesList numbXedges = len(evix) findexDict = {} # faces(key=face index) which have two edges (value=xCoor index) for i in range(numbXedges) : ei = evix[i] efiArr = efi[ei] for fi in efiArr : if fi in findexDict : curVal = findexDict[fi] if isinstance(curVal,list) : # DevNote: this modifications is a consequence of quadrilateral # faces, which may have more than one intersectioon to a plane. # This only eliminates runtime error. Solution is to use a surface # with lrez > 0, then generated contours. newVal = [curVal[0],i] else : newVal = [curVal,i] findexDict[fi] = newVal else : findexDict[fi] = i # remove face interset points if face only has one intersect edge # ( intersect may be on a vertex ) # test = [ x for x in findexDict.values() if type(x) is list ] if len(test) == 0 : return None,None,None # face, coorInx = [], [] for key,value in findexDict.items() : if type(value) is list : valueX = order_value(key,value,xCoor,projVect) coorInx.append(valueX) face.append(key) if len(coorInx) == 0 : return None,None,None return np.array(xCoor), coorInx, face # --------------------------------------------------------------- def getColors(colors, indices) : con_colors = [None]*len(indices) for i in range(len(indices)) : con_colors[i] = colors[indices[i]] return con_colors # --------------------------------------------------------------- # get the surface colors which will be applied to the default contour colors self._postProc_surfaceColors() surf_colors = self._dfacecolors # update for v_1.2.0 if type(args[0]) is tuple : args=args[0] line = None for aLen in args : vertexCoor,segmIndices,faceIndices = getLineVals( dotprod,aLen,projVect ) if vertexCoor is None : warnings.warn( 'Surface contourLines not found for dist value of {}'.format(aLen) ) continue tline = ColorLine3DCollection(vertexCoor,segmIndices) if tline is None : continue tline.set_color(getColors(surf_colors,faceIndices)) # default surface color if line is None: line = tline else: line += tline return line def _transformVector(self, orgCoor, operation, returnxyz) : """ Functional transformation of coordinates. """ xyz = np.transpose(orgCoor) abc = self.coor_convert(xyz) rst = np.array(operation(abc)) if returnxyz : XYZ = rst else : XYZ = self.coor_convert( rst , tocart=True ) return np.transpose(XYZ) def _get_vectorfield_Line3DCollection(self, vector_field, color, width, alr) : """Vector3DCollection at surface vertices.""" self.vector_field = vector_field lcol = Vector3DCollection(self.vertexCoor,vector_field,alr,colors=color, linewidths=width) # --- 1.1 mod -------------------- lcol.coorType = self.coorType lcol.uvwOrientationType = self.coorType return lcol def _get_vertex_normals(self) : """Unit normals of at vertex coordinates. """ # The normalized unweighted average of the surface # normals of the faces that contain that vertex. verts = self.vertexCoor[ self.fvIndices ] faceNormalCoor = self._get_face_normals(verts) # note: following list are the face indices that each vertex is a member. # Since the number of faces a vertex is attached ranges from 1 - 6, # needs a list, not an np.array. vfIndicesList = [ None ] *len(self.vertexCoor) for i in range(len(vfIndicesList)) : vfIndicesList[i] = [] for fInx in range( len(self.fvIndices) ) : face = self.fvIndices[fInx] for vInx in face : vfIndicesList[vInx].append(fInx) # DevNote: probably a 'cleaner' method? MROI normals = [ None ] *len(self.vertexCoor) for i in range(len(normals)) : vert = vfIndicesList[i] vertList = faceNormalCoor[ vert ] vertSum = np.sum(vertList,axis=0)/len(vertList) unitVector = np.divide( vertSum, np.linalg.norm(vertSum) ) normals[i] = unitVector normals = np.array(normals) return normals def _get_face_normals(self,faceCoor,ausf=None) : """ Unit normals of triangular face coordinates.""" vfT = np.transpose(faceCoor, (1,2,0) ) vAB = np.subtract( vfT[1], vfT[0] ) vAC = np.subtract( vfT[2], vfT[0] ) vABt = np.transpose(vAB) vACt = np.transpose(vAC) if ausf is not None: vABt = vABt*ausf vACt = vACt*ausf cross = np.cross(vABt,vACt) sz = np.linalg.norm(cross,axis=1)[:,np.newaxis] return np.divide(cross,sz) def _get_face_centers(self,faceCoor) : """ Face centers from face vertex coordinates. """ vfT = np.transpose(faceCoor, (1,2,0) ) numVerts = vfT.shape[0] sumV = np.sum(vfT, axis=0) return np.transpose(sumV)/numVerts def _set_geometric_bounds(self) : """ set the bounds dictionary from surface vertex coordinates. """ _set_geomBounds(self.vertexCoor,self._bounds) return def _viewportCoor(self,xyzCoor,viewport=None) : """ UV coordinates for image texture mapping """ # better way to do this? MROI xyz = np.transpose(xyzCoor) ab = self._map3Dto2Dsquare( xyz ) a,b = ab if viewport is None : return a,b,np.full(len(a),True) As,Bs,Ae,Be = viewport Aref = 1.0-As Bref = 1.0-Bs Adelta = np.mod(Ae+Aref,1.0) Bdelta = np.mod(Be+Bref,1.0) if Adelta <=0.0 : Adelta = 1.0 if Bdelta <=0.0 : Bdelta = 1.0 Ap = np.mod(a+Aref,1.0) Bp = np.mod(b+Bref,1.0) Av = np.divide(Ap,Adelta) Bv = np.divide(Bp,Bdelta) inViewport = Av<=1 inViewport = np.where(Bv>1,np.full(len(inViewport),False),inViewport) Av = np.clip(Av,0.0,1.0) Bv = np.clip(Bv,0.0,1.0) return Av,Bv,inViewport def _triangulate_faceIndices(self,fvIndx) : fvIndices, _ = self._triangulate_faceIndices_faceColors(fvIndx) return fvIndices def _triangulate_faceIndices_faceColors(self,fvIndx,faceColors=None) : """ Subdivide all faces into triangles. Two uses with number face edges is 3,4,5 or 6: 1. initial triangulate collection of faces having a differnt number of edges called from __init__ with faceIndices passed as a list. 2. triangulate collection of faces with identical number of edges called from triangulate where faceIndicess is a numpy array """ # ............................................................. def divideFace4(face) : dist_a = np.linalg.norm( self.vertexCoor[ face[0]] - self.vertexCoor[ face[2]] ) dist_b = np.linalg.norm( self.vertexCoor[ face[1]] - self.vertexCoor[ face[3]] ) if dist_a < dist_b : face1 = [ face[0], face[1], face[2] ] face2 = [ face[2], face[3], face[0] ] else : face1 = [ face[1], face[2], face[3] ] face2 = [ face[3], face[0], face[1] ] return face1, face2 def divideFace5(face) : diag = lambda i : self.vertexCoor[ face[i]] - self.vertexCoor[ face[(i+2)%5] ] fc3 = lambda i : [ face[i], face[(i+1)%5], face[(i+2)%5] ] fc4 = lambda i : [ face[(i+2)%5], face[(i+3)%5], face[(i+4)%5], face[(i+5)%5] ] dist = [None]*5 for i in range(5) : dist[i] = np.linalg.norm( diag(i) ) minDiag = 0 for i in range(1,5) : if dist[i] < dist[minDiag] : minDiag = i face1 = fc3(minDiag) otherFace = fc4(minDiag) face2, face3 = divideFace4(otherFace) return face1, face2, face3 def divideFaces6(face) : # .. not the best, but easiest (could calc midpoint). MROI dist = [None]*3 dist[0] = np.linalg.norm( self.vertexCoor[ face[0]] - self.vertexCoor[ face[3]] ) dist[1] = np.linalg.norm( self.vertexCoor[ face[1]] - self.vertexCoor[ face[4]] ) dist[2] = np.linalg.norm( self.vertexCoor[ face[2]] - self.vertexCoor[ face[5]] ) iMin = 0 if dist[1] < dist[0] : iMin = 1 if dist[2] < dist[iMin] : iMin = 2 faceA = [ face[iMin], face[ iMin+1 ], face[ iMin+2 ], face[ iMin+3 ] ] faceB = [ face[iMin+3], face[(iMin+4)%6], face[(iMin+5)%6], face[(iMin+6)%6] ] face1, face2 = divideFace4(faceA) face3, face4 = divideFace4(faceB) return face1, face2, face3, face4 # ............................................................. # FutDev: easy to understand but need to cleanup, MROI subFaceIndices = [] fColor = [] setColors = False if faceColors is not None: setColors = True if len(fvIndx) != len(faceColors) : # if faceColors, then this should not happen, but report if so.... warnings.warn('Internal ERROR: tfc001') return None for i, face in enumerate(fvIndx) : numVerts = len(face) if numVerts == 3 : subFaceIndices.append(face) if setColors : fColor += [faceColors[i]]*1 elif numVerts == 4 : f1, f2 = divideFace4(face) subFaceIndices.append(f1) subFaceIndices.append(f2) if setColors : fColor += [faceColors[i]]*2 elif numVerts == 5 : f1, f2, f3 = divideFace5(face) subFaceIndices.append(f1) subFaceIndices.append(f2) subFaceIndices.append(f3) if setColors : fColor += [faceColors[i]]*3 elif numVerts == 6 : f1, f2, f3, f4 = divideFaces6(face) subFaceIndices.append(f1) subFaceIndices.append(f2) subFaceIndices.append(f3) subFaceIndices.append(f4) if setColors : fColor += [faceColors[i]]*4 return np.array(subFaceIndices), fColor def _get_edges_from_faces(self,fvIndx) : """ Used in __init__ if edgeIndices=None to create edgeIndices. """ # ............................................................. def edges_from_faceList( fvIndx ) : # NOTE: very poor execution time. (~2 orders-of-magnitude slower) # face index list of lists can't be used as a Numpy array # since edge list of multi-type faces. warnings.warn('multi-type polyhedron faces: use of non-optimized edge calc.') keyList, edgeList = [],[] strkey = lambda a,b : str(max(a,b)) + '_' + str(min(a,b)) for face in fvIndx : numEdges = len(face) for i in range(numEdges) : a = face[i] b = face[ (i+1)%numEdges] key = strkey(a,b) if key not in keyList : keyList.append(key) edgeList.append([a,b]) return np.array(edgeList) # ............................................................. # following line only needed when called for mixed polygon faces if fvIndx.ndim == 1 : return edges_from_faceList(fvIndx) numEdges = fvIndx.shape[1] indicies = [ (i+1)%numEdges for i in range(numEdges) ] ofs_fvIndx = fvIndx[:,indicies] stepA = np.stack( (fvIndx,ofs_fvIndx), axis=-1 ) stepB = np.reshape( stepA, (-1,2 ) ) stepC = np.sort(stepB,axis=1) u, indices = np.unique(stepC, axis=0, return_index=True) edgeList = stepC[indices] return edgeList @property def _dfacecolors(self) : """ Direct access to the private property _facecolors. Appears that the self.get_facecolor() needs a call to the self.do_3d_projection() which will 'sort the 2D version by view depth'. for render construction. Need the original order of colors. BANDAID fix: update for v_1.2.0 FutDev: define a separate private property to hold these values and not use the Matplotlib inherited value? MROI """ return self._facecolors @property def _dedgecolors(self) : """ Direct access to the private property _edgecolor. """ return self._edgecolors @property def vlim(self) : '''Range of values associated with color''' return self._bounds['vlim'] @vlim.setter def vlim(self,val) : self._bounds['vlim'] = val return @property def name(self) : '''Descriptive identifier for the surface geometry.''' if self._geomName is None : return '' return self._geomName @name.setter def name(self, val) : self._geomName = val self.set_label( '' if val is None else val ) return @property def cname(self) : '''Descriptive identifier for values indicated by color.''' if self.valuesName is None : return '' return self.valuesName @cname.setter def cname(self, val) : self.valuesName = val return @property def bounds(self) : """ Dictionary of surface geometric and value ranges. Each dictionary value is a 2 float array of minimum and maximum values of the surface. Keys are: 'xlim' : x-coordinate 'ylim' : y-coordinate 'zlim' : z-coordinate 'r_xy' : radial distance from the z axis 'rorg' : radial distance from the origin 'vlim' : value functional assignments. Values are assigned from the geometry and color mapping methods, including clipping. """ return self._bounds @property def cBar_ScalarMappable(self) : """matplotlib.cm.ScalarMappable object for surface values.""" vmin, vmax = self._bounds['vlim'] norm = colors.Normalize(vmin=vmin,vmax=vmax) objMap = self.get_cmap() sm = cm.ScalarMappable(cmap=objMap, norm=norm) sm.set_array([]) return sm @property def edges(self) : """ColorLine3DCollection of the surface edges from facecolors.""" # ...................................................... def get_edge_color() : efcList = copy.copy(self.efIndicesList) # an edge may have 1 or 2 adjacent faces.... for i,cList in enumerate(self.efIndicesList) : if len(cList)==1 : efcList[i].append(efcList[i][0]) efcArray = np.array(efcList) self._postProc_surfaceColors(True) colorA = self.facecolors[efcArray.T[0]] colorB = self.facecolors[efcArray.T[1]] # average of adjacent face RGB colors return ( colorA+colorB)/2 # ...................................................... v = self.vertexCoor e = self._get_edges_from_faces(self.fvIndices) # accounts for clipping... lcol = ColorLine3DCollection(v,e,'edges') lcol._ledgecolors = get_edge_color() # update for v_1.2.0 return lcol @property def initedges(self) : ''' ColorLine3DCollection of the initial surface edges. ONLY available for surfaces where the number of face edges differ among faces and the number of faces is less than 250. ''' if self._initedges is None : raise ValueError('initedges property not available, all faces have equal number of edges (use edges property)') e = self._initedges v = self._initverts lcol = ColorLine3DCollection(v,e,'edges') return lcol @property def vertices(self) : """A 3 x N array of surface vertices.""" v = self.vertexCoor m = np.transpose(v) return m @property def facecenters(self) : """A 3 x N array of surface face center coordinates.""" verts = self.vertexCoor[ self.fvIndices ] faceCenterCoor = self._get_face_centers(verts) m = np.transpose(faceCenterCoor) return m @property def facecolors(self) : """A N x 4 array of surface face colors.""" self._postProc_surfaceColors() return self._dfacecolors # update for v_1.2.0 @property def svd_dict(self) : """ A dictionary of results from a Singular Value Decomposition of the data. Keys are: 'disarr' : array of N floats, normalized to the surface size. 'sigma' : standard deviation 'trans' : [rotation matrix, scaling, translation] Values are assigned using the map_geom_from_svd method. The dictionary is None if the method has not been called by the surface object. """ return self._svd_dict @property def area_h2b(self) : """ A 2 x N array of normalized N face areas and shapes.""" f,v = self.fvIndices, self.vertexCoor if f.shape[1] == 3 : # triangular A = v[ f[:,1] ] - v[ f[:,0] ] B = v[ f[:,2] ] - v[ f[:,0] ] C = v[ f[:,2] ] - v[ f[:,1] ] area = 0.5*np.linalg.norm( np.cross(A,B),axis=1 ) aveArea = np.average(area) normArea = area/aveArea edges = np.linalg.norm( np.array( [A,B,C]),axis=2) maxsizes = np.amax(edges,axis=0) f = 4*np.sqrt(3)/3 # equilateral triangle skew = f*area/(maxsizes*maxsizes) else : # quadrateral is two triangles. b02 = v[ f[:,2] ] - v[ f[:,0] ] b13 = v[ f[:,3] ] - v[ f[:,1] ] base = np.array( [b02,b13] ) diag = np.linalg.norm( base ,axis=2) i = np.where(diag[0]>diag[1], np.zeros(len(f),int), np.ones(len(f),int) ) maxsizes = diag[i,1] A = b02 # top triangle .... B = v[ f[:,3] ] - v[ f[:,0] ] areaTop = 0.5*np.linalg.norm( np.cross(A,B),axis=1 ) # bottom triangle .... B = v[ f[:,1] ] - v[ f[:,2] ] areaBtm = 0.5*np.linalg.norm( np.cross(-A,B),axis=1 ) area = np.add( areaTop,areaBtm) aveArea = np.average(area) normArea = area/aveArea f = 1 # square skew = f*area/(maxsizes*maxsizes) # .................... return [normArea,skew]
[docs] def triangulate(self, rez=0) : """ PLANAR subdivision of each face into triangular faces. Parameters ---------- rez : integer, optional, default: 0 Number of recursive subdivisions of the base faces into triangular faces, four faces per rez. Rez values range from 0 to 7. For surfaces with 4 vertices per face, each face is first subdivided into two faces, then recursion proceeds. For surfaces with 5 vertices per face, each face is first subdivided into three faces, then recursion proceeds. Returns ------- self : surface object """ # ............................................................. def midVFun(vectA, vectB) : mid = np.add(vectA,vectB) mid = np.multiply(0.5,mid) return mid # ............................................................. self._postProc_surfaceColors(True) if self.fvIndices.shape[1] > 3 : # if needed, break faces into triangles before proceeding.... subDiv = self.fvIndices.shape[1] - 2 self.fvIndices = self._triangulate_faceIndices(self.fvIndices) newFaceColors = [] for fColor in self._dfacecolors : # update for v_1.2.0 newFaceColors += [fColor]*subDiv #self.set_facecolor( np.array(newFaceColors) ) self.set_color( np.array(newFaceColors) ) # update for v_1.3.0 # .... update for v_1.3.0 #indexObj ,vertexCoor, vertexValues, phongNorms = _triangulateBase(self,rez,midVFun) indexObj ,vertexCoor, vertexValues, phongNorms = self._triangulateBase(rez,midVFun) self.vertexVals = vertexValues self.phongNorms = phongNorms # ....................... self.fvIndices = indexObj['face'] self.vertexCoor = vertexCoor self.vertexVals = vertexValues self.evIndices = self._get_edges_from_faces(self.fvIndices) self.vfIndicesList = self._vertexCommonFaceIndices() self.efIndicesList = self._edgeCommonFaceIndices() v = self.vertexCoor verts = v[self.fvIndices] self.set_verts( verts ) self._set_geometric_bounds() if rez != 0 : newFaceColors = [] subDiv = 4**rez for fColor in self._dfacecolors : # update for v_1.2.0 newFaceColors += [fColor]*subDiv #self.set_facecolor( np.array(newFaceColors) ) self.set_color( np.array(newFaceColors) ) # update for v_1.3.0 return self
[docs] def normalize_scale(self) : """Scaling normalization array and reciprocal.""" bounds = self.bounds scX = ( bounds['xlim'][1] - bounds['xlim'][0] ) scY = ( bounds['ylim'][1] - bounds['ylim'][0] ) scZ = ( bounds['zlim'][1] - bounds['zlim'][0] ) recip = [scX,scY,scZ] scale = np.reciprocal(recip) return scale, recip
[docs] def vertexnormals(self, **kargs) : """ Unit directions or Vector3DCollection of vertex normals at vertices. Parameters ---------- scale: number, optional If not spectified, scaled proportional to the mean edge length of surface base faces. v3d: boolean, optional, default: True If True, Vector3DCollection is returned, else a N x 3 array of unit vector vertex normals is returned if False. color : str or float list of length 3 or 4. RGB or RGBA color, either a Matplotlib format string or a list of color values in range [0,1]. width : number, optional, default: 1 Line width of the vector. alr : scalar, optional, default: 0.25 Axis length ratio, head size to vector magnitude. Returns ------- Vector3DCollection object or array of unit direction vectors """ kargDft = { 'scale':self._normal_scale, 'v3d':True, 'color':None, 'width':1, 'alr':_DFT_ALR } KW = KWprocessor(kargDft,'vectorfield_from_op',**kargs) scale = KW.getVal('scale') isV3D = KW.getVal('v3d') color = KW.getVal('color') width = KW.getVal('width') alr = KW.getVal('alr') verts = self.vertexCoor norms = self._get_vertex_normals() if not isV3D : return norms name = 'vertex_normals' lcol = Vector3DCollection(verts,norms*scale,alr,name,colors=color, linewidths=width) lcol.coorType = self.coorType lcol.uvwOrientationType = _COORSYS["XYZ"] if color is None : lcol._set_vectColor( self.vertexColor ) return lcol
[docs] def facenormals(self, **kargs) : """ Unit directions or Vector3DCollection of face normals at face centers. Parameters ---------- scale: number, optional If not spectified, scaled proportional to the mean edge length of surface base faces. v3d: boolean, optional, default: True If True, Vector3DCollection is returned, else a N x 3 array of unit vector face normals is returned if False. color : str or float list of length 3 or 4. RGB or RGBA color, either a Matplotlib format string or a list of color values in range [0,1]. width : number, optional, default: 1 Line width of the vector. alr : scalar, optional, default: 0.25 Axis length ratio, head size to vector magnitude. Returns ------- Vector3DCollection object or array of unit direction vectors """ #Note: default color:None so that can use as a flag to use face colors if not defined. kargDft = { 'scale':self._normal_scale, 'v3d':True, 'color':None, 'width':1, 'alr':_DFT_ALR } KW = KWprocessor(kargDft,'vectorfield_from_op',**kargs) scale = KW.getVal('scale') isV3D = KW.getVal('v3d') color = KW.getVal('color') width = KW.getVal('width') alr = KW.getVal('alr') verts = self.vertexCoor[ self.fvIndices ] faceCenterCoor = self._get_face_centers(verts) faceNormalCoor = self._get_face_normals(verts) if not isV3D : return faceNormalCoor name = 'face_normals' lcol = Vector3DCollection(faceCenterCoor,faceNormalCoor*scale,alr,name,colors=color, linewidths=width) # --- 1.1 mod -------------------- lcol.coorType = self.coorType lcol.uvwOrientationType = _COORSYS["XYZ"] if color is None : lcol._set_vectColor(self._dfacecolors) # update for v_1.2.0 return lcol
[docs] def dispfield_from_op(self, operation, **kargs) : """ Vector3DCollection of displacement of vertices to a different position. Parameters ---------- operation : function object Function that takes one coordinate argument, a 3xN Numpy array. The function returns a 3xN array of vectors. returnxyz : bool { True, False }, optional, default: False By default, native coordinates are returned by the operation function. If set True, the operation returns xyz Cartesian coordinates. useBase : bool { True, False }, optional, default: False When set False, vertices of the surface, prior to any geometric mapping or transforms, are passed to the operation function. Otherwise, when True, the current surface vertices are passed. scale: number, optional, default: 1. color : str or float list of length 3 or 4. RGB or RGBA color, either a Matplotlib format string or a list of color values in range [0,1]. width : number, optional, default: 1 Line width of the vector. alr : scalar, optional, default: 0.25 Axis length ratio, head size to vector magnitude. Returns ------- Vector3DCollection object """ kargDft = { 'returnxyz':False, 'useBase':False, 'scale':1, 'color':'black', 'width':1, 'alr':_DFT_ALR } KW = KWprocessor(kargDft,'dispfield_from_op',**kargs) returnxyz = KW.getVal('returnxyz') useBase = KW.getVal('useBase') scale = KW.getVal('scale') color = KW.getVal('color') width = KW.getVal('width') alr = KW.getVal('alr') if self._isCompositeSurface : warnings.warn('Vector operation not available for combined shapes.') return start_coor = self.vertexCoor if useBase : start_coor = self.baseVertexCoor v = self._transformVector(start_coor, operation, returnxyz) delta = v - self.vertexCoor delta = scale*delta # --- 1.1 mod -------------------- vField = self._get_vectorfield_Line3DCollection(delta, color, width, alr) if returnxyz : vField.uvwOrientationType = _COORSYS["XYZ"] return vField
[docs] def vectorfield_from_op(self, operation, **kargs) : """ Vector3DCollection of vectors in u,v,w coordinates at surface vertices. Parameters ---------- operation : function object Function that takes one coordinate argument, a 3xN Numpy array of native coordinates. The function returns a 3xN array of vectors. scale: number, optional, default: 1. color : str or float list of length 3 or 4. RGB or RGBA color, either a Matplotlib format string or a list of color values in range [0,1]. width : number, optional, default: 1 Line width of the vector. alr : scalar, optional, default: 0.25 Axis length ratio, head size to vector magnitude. Returns ------- Vector3DCollection object """ kargDft = { 'scale':1, 'color':'black', 'width':1, 'alr':_DFT_ALR } KW = KWprocessor(kargDft,'vectorfield_from_op',**kargs) scale = KW.getVal('scale') color = KW.getVal('color') width = KW.getVal('width') alr = KW.getVal('alr') if self._isCompositeSurface : warnings.warn('Vector operation not available for combined shapes.') return xyz = np.transpose(self.vertexCoor) abc = self.coor_convert(xyz) abc = np.array(abc) rst = np.array(operation(abc)) rst = scale*rst delta = self._uvw_to_xyz(rst,abc) # --- 1.1 mod -------------------- delta = np.array(delta) return self._get_vectorfield_Line3DCollection(delta, color, width, alr)
[docs] def vectorfield_to_surface(self, surface, **kargs) : """ Vector3DCollection of vectors from surface to surface vertex coordinates. Parameters ---------- surface : surface object Surface that matches the calling surface vectors. Should be the same basetype and rez for surface subclasses. scale: number, optional, default: 1. color : str or float list of length 3 or 4. RGB or RGBA color, either a Matplotlib format string or a list of color values in range [0,1]. width : number, optional, default: 1 Line width of the vector. alr : scalar, optional, default: 0.25 Axis length ratio, head size to vector magnitude. Raises ------ ValueError Mismatched surfaces based on the number of vertices. Returns ------- Vector3DCollection object """ kargDft = { 'scale':1, 'color':'black', 'width':1, 'alr':_DFT_ALR } KW = KWprocessor(kargDft,'vectorfield_to_surface',**kargs) scale = KW.getVal('scale') color = KW.getVal('color') width = KW.getVal('width') alr = KW.getVal('alr') surLen, selfLen = len(surface.vertexCoor) , len(self.vertexCoor) #if surLen is not selfLen : # 1.1 err. corrected if surLen != selfLen : raise ValueError('Surfaces have unequal number of vertices, {} != {}'.format(surLen, selfLen)) delta = surface.vertexCoor - self.vertexCoor delta = scale*delta # --- 1.1 mod -------------------- vField = self._get_vectorfield_Line3DCollection(delta, color, width, alr) vField.uvwOrientationType = _COORSYS["XYZ"] return vField
def _map_color_from_image_vert(self, img, viewport ) : a,b,inViewport = self._viewportCoor(self.vertexCoor,viewport) height, width = np.subtract(img.shape[:2],1) M_index = ( (1-b)*height ).astype(int) N_index = ( a*width ).astype(int) #''' orig_colors = self.vertexColor if len(orig_colors) == 1 : onearr = np.ones( len(self.vertexCoor) )[:, np.newaxis] orig_colors = orig_colors*onearr colorMap = [] oneAlp = np.array([1.0]) if viewport is None : colorMap = img[M_index,N_index] else : # FutDev: use numpy methods to optimize efficiency? for i in range( len(orig_colors) ) : colorMap.append(orig_colors[i]) if inViewport[i] : temp = img[M_index[i],N_index[i]] #.. note: img may or may not have an alpha channel.. temp2 = temp[0:3] colorMap[i] = np.concatenate((temp2,oneAlp)) self.vertexColor = np.array(colorMap) return
[docs] def map_color_from_image(self, fname, viewport=None) : """ Assign image color values to surface face colors. This method is not available for composite surfaces. The entire image domain is applied to the coloring operation. Parameters ---------- fname : str or file-like The image file to read: a filename, a URL or a file-like object opened in read-binary mode. Currently restricted to PNG format. viewport : 4D array-like, optional, default: None Viewport defines the subdomain of the surface onto which the image is mapped. The subdomain is set by normalized native surface coordinates in a 4D array. The entire surface is colored for the default of None. Returns ------- self : surface object """ if self._isCompositeSurface : warnings.warn('Image operation not available for combined shapes.') return self if self._surfaceShapeChanged : warnings.warn('Image operation may be anomalous after shape modification.') if viewport is not None : viewport = np.array(viewport) if np.any( (viewport<0) | (viewport>1) ) : raise ValueError('viewport list values, {}, must be between 0 and 1'.format(viewport)) if len(viewport) != 4 : raise ValueError('viewport list must be length 4, found {}'.format(len(viewport))) verts = self.vertexCoor[ self.fvIndices ] faceCenterCoor = self._get_face_centers(verts) a,b,inViewport = self._viewportCoor(faceCenterCoor,viewport) img = image.imread(fname) height, width = np.subtract(img.shape[:2],1) M_index = ( (1-b)*height ).astype(int) N_index = ( a*width ).astype(int) orig_colors = self._dfacecolors # update for v_1.2.0 if len(orig_colors) == 1 : onearr = np.ones( len(faceCenterCoor) )[:, np.newaxis] orig_colors = orig_colors*onearr colorMap = [] if viewport is None : colorMap = img[M_index,N_index] else : # FutDev: use numpy methods to optimize efficiency? for i in range( len(orig_colors) ) : colorMap.append(orig_colors[i]) if inViewport[i] : colorMap[i] = img[M_index[i],N_index[i]] self.set_color(colorMap) self.isSurfaceColored = True self.vertexValues = None # ======================================================================== self._map_color_from_image_vert(img,viewport ) # ======================================================================== return self
def _map_color_from_op_vert(self, operation, rgb=True) : xyz = np.transpose(self.vertexCoor) abc = self.coor_convert(xyz) colors = np.array(operation(abc)) colors = np.transpose(colors) colors = np.clip(colors,0,1) if rgb : RGB = colors else : RGB = cm.colors.hsv_to_rgb(colors) if RGB.shape[1] == 3 : # add alpha channel. ones = np.array( [np.ones( RGB.shape[0] )] ).T RGB = np.concatenate( (RGB, ones ), axis=1 ) self.vertexColor = RGB self._bounds['vertvlim'] = _DFT_VERTVLIM return
[docs] def map_color_from_op(self, operation, rgb=True, cname=None) : """ Assignment of face color from a function. Face colors are assigned from a function of face coordinates. Parameters ---------- operation : function object Function that takes one argument, a 3xN Numpy array of native coordinates. The function returns a 3xN color value. rgb : bool {True, False}, optional, default: True By default, RGB color values are returned by the operation function. If set False, the operation returns HSV color values. cname : string, optional, default: None or op function name. Descriptive identifier for values indicated by color. Returns ------- self : surface object """ verts = self.vertexCoor[ self.fvIndices ] faceCenterCoor = self._get_face_centers(verts) xyz = np.transpose(faceCenterCoor) abc = self.coor_convert(xyz) colors = np.array(operation(abc)) colors = np.transpose(colors) colors = np.clip(colors,0,1) if rgb : RGB = colors else : RGB = cm.colors.hsv_to_rgb(colors) self.set_color(RGB) self.isSurfaceColored = True cname = _getFunctionName(operation,cname) if cname is not None : self.valuesName = cname self._bounds['vlim'] = _DFT_VLIM self._map_color_from_op_vert(operation, rgb) return self
def _map_cmap_from_datagrid_vert(self, datagrid, cmap=None, viewport=None) : a,b,inViewport = self._viewportCoor(self.vertexCoor,viewport) data = datagrid dmax = np.amax(datagrid) dmin = np.amin(datagrid) xd = np.linspace(0, 1, data.shape[0] ) yd = np.linspace(0, 1, data.shape[1] ) g = interpolate.interp2d(yd, xd, data, kind='cubic') d = [] # FutDev: use numpy methods to optimize efficiency. for ab in np.transpose([a,b]) : d.append( g(ab[0],ab[1])[0] ) d = np.where(inViewport,d,np.full(len(d),dmin)) if cmap is not None : if isinstance(cmap,str) : self.set_cmap(cm.get_cmap(cmap)) else : self.set_cmap(cmap) cmap = self.get_cmap() orig_colors = self.vertexColor if len(orig_colors) == 1 : onearr = np.ones( len(self.vertexCoor) )[:, np.newaxis] orig_colors = orig_colors*onearr norm = colors.Normalize(dmin,dmax) temp = cmap(norm(d)) if viewport is None : colorMap = temp else : colorMap = np.where(inViewport[:, np.newaxis], temp, orig_colors ) # FutDev: set_color used instead of set_facecolor to eliminate 'gaps' between faces, <<<<< # however, set edgecolor required after illumination if edges are to be displayed. <<<<< # HOWEVER !!!! if colorMap has a alpha<1, these don't register with the edges. <<<<< #self.vertexValues = d <<<< NO.. interpolated values only in viewport, self.vertexColor = np.array(colorMap) return
[docs] def map_cmap_from_datagrid(self, datagrid, cmap=None, viewport=None, cname=None) : """ Face color assignment using a 2D datagrid surface. Datagrid values are normalized in the range 0 to 1. This method is not available for composite surfaces. The entire datagrid domain is applied to the geometric operation. Parameters ---------- datagrid : 2D float array cmap : str or Colormap, optional A Colormap instance or registered colormap name. If not assigned, the surface Colormap is used. The colormap maps the datagrid values to colors. viewport : 4D array-like, optional, default: None Viewport defines the subdomain of the surface onto which the datagrid is mapped. The subdomain is set by normalized native surface coordinates in a 4D array. The entire surface is colored for the default of None. cname : string, optional, default: None. Descriptive identifier for values indicated by color. Returns ------- self : surface object """ if self._isCompositeSurface : warnings.warn('Datagrid operation not available for combined shapes.') return self if self._surfaceShapeChanged : warnings.warn('Datagrid operation may be anomalous after shape modification.') verts = self.vertexCoor[ self.fvIndices ] faceCenterCoor = self._get_face_centers(verts) a,b,inViewport = self._viewportCoor(faceCenterCoor,viewport) data = datagrid dmax = np.amax(datagrid) dmin = np.amin(datagrid) xd = np.linspace(0, 1, data.shape[0] ) yd = np.linspace(0, 1, data.shape[1] ) g = interpolate.interp2d(yd, xd, data, kind='cubic') d = [] # FutDev: use numpy methods to optimize efficiency. for ab in np.transpose([a,b]) : d.append( g(ab[0],ab[1])[0] ) d = np.where(inViewport,d,np.full(len(d),dmin)) if cmap is not None : if isinstance(cmap,str) : self.set_cmap(cm.get_cmap(cmap)) else : self.set_cmap(cmap) cmap = self.get_cmap() orig_colors = self._dfacecolors # update for v_1.2.0 if len(orig_colors) == 1 : onearr = np.ones( len(faceCenterCoor) )[:, np.newaxis] orig_colors = orig_colors*onearr norm = colors.Normalize(dmin,dmax) temp = cmap(norm(d)) if viewport is None : colorMap = temp else : colorMap = np.where(inViewport[:, np.newaxis], temp, orig_colors ) # FutDev: set_color used instead of set_facecolor to eliminate 'gaps' between faces, # however, set edgecolor required after illumination if edges are to be displayed. # HOWEVER !!!! if colorMap has a alpha<1, these don't register with the edges. self.set_color(colorMap) self.isSurfaceColored = True self._bounds['vlim'] = [ dmin, dmax ] self.valuesName = cname # ======================================================================== self._map_cmap_from_datagrid_vert(datagrid, cmap, viewport ) # ======================================================================== return self
def _getColorMap_fromNormals(self,fvo,cmap, direction, isAbs) : """ used by map_cmap_from_normals() & _map_cmap_from_normals_vert() """ unitVector = lambda v : np.divide( v, np.linalg.norm(v) ) incidentLight = unitVector( direction ) d = np.dot(fvo,incidentLight) if isAbs : v = 1 - np.abs(d) else : v = ( d + 1 )/2 v = np.abs(v) # to insure positive values near zero colorMap = cmap(v) return colorMap def _map_cmap_from_normals_vert(self, cmap, direction, isAbs) : vobj = self._get_vertex_normals() cMap = self._getColorMap_fromNormals(vobj, cmap, direction, isAbs) self.vertexColor = np.array(cMap) return
[docs] def map_cmap_from_normals(self, cmap=None, direction=None, cname=None, isAbs=False) : """ Face color assignment using normals relative to a direction. The dot product of face normals with the direction is used to assign face colors from a colormap. Parameters ---------- cmap : str or Colormap, optional A Colormap instance or registered colormap name. If not assigned, the surface Colormap is used. The colormap maps the dot product values to colors. direction : list of size 3, optional, default: [1,0,1] A 3D vector in xyz Cartesian coordinates designating the direction of the illumination source. If assigned the Axes3D, will use the view direction. cname : string, optional, default: None. Descriptive identifier for values indicated by color. isAbs : boolean, optional, default: False If set True, the absolute value of the dot product between face normals and illumination direction is used. Returns ------- self : surface object """ if direction is not None: temp = direction try : # check if direction holds the elev, azim (ie. 3D axis) elev,azim = temp.elev, temp.azim direction = elev_azim_2vector(elev,azim) except : direction = temp if len(direction) != 3 : raise ValueError('Cmap from normals direction must be a array of length 3, or a Matplotlib Axes3D object.') else : direction = _ILLUM if cmap is not None : if isinstance(cmap,str) : self.set_cmap(cm.get_cmap(cmap)) else : self.set_cmap(cmap) cm_colorMap = self.get_cmap() verts = self.vertexCoor[ self.fvIndices ] fvobj = self._get_face_normals(verts) cMap = self._getColorMap_fromNormals(fvobj, cm_colorMap, direction, isAbs) self.set_cmap(cm_colorMap) self.set_color(cMap) self.isSurfaceColored = True self.onlyColoredFaces = True self.valuesName = cname # ======================================================================== self._map_cmap_from_normals_vert( cm_colorMap, direction, isAbs ) # ======================================================================== return self
[docs] def map_cmap_from_vertvals(self,cmap=None, cname=None) : """ Face color assignment using face vertex values. Parameters ---------- cmap : str or Colormap, optional A Colormap instance or registered colormap name. If not assigned, the surface Colormap is used. The colormap maps the datagrid values to colors. cname : string, optional, default: None. Descriptive identifier for values indicated by color. Returns ------- self : surface object """ if cmap is not None : if isinstance(cmap,str) : self.set_cmap(cm.get_cmap(cmap)) else : self.set_cmap(cmap) cmap = self.get_cmap() if self.vertexVals is None : warnings.warn('ERROR: [map_cmap_from_vertvals] '+ 'vertice values are unassigned, no action taken.') return self fvIndices = self.fvIndices vertVals = self.vertexVals fvIndices = np.array(fvIndices) fvertVals = vertVals[fvIndices] v = np.average(fvertVals,axis=1) vmin, vmax = v.min(), v.max() self.faceValues = v self._bounds['vlim'] = [ vmin, vmax ] norm = colors.Normalize(vmin=vmin,vmax=vmax) colorMap = cmap(norm(v)) self.set_color(colorMap ) self.isSurfaceColored = True self.valuesName = cname return self
[docs] def set_vertvals(self,values) : """ Assign scalar values to each vertex. Parameters ---------- values : list or array of length N, where N is the number of vertices. Returns ------- self : surface object """ values = np.array(values) noVerts = self.vertexCoor.shape[0] noVals = values.shape[0] if noVals != noVerts : warnings.warn("Error: [set_vertvals] no. vertices {} != no. values {}, no action taken".format(noVerts,noVals)) return self self.vertexVals = values return self
def _map_cmap_from_op_vert(self, operation, cmap) : xyz = np.transpose(self.vertexCoor) # ..... abc = self.coor_convert(xyz) v = np.array(operation(abc)) self.vertexValues = v norm = colors.Normalize(vmin=v.min(),vmax=v.max()) colorMap = cmap(norm(v)) self.vertexColor = colorMap colorMap = np.array(colorMap) self._bounds['vertvlim'] = [ v.min(), v.max() ] return
[docs] def map_cmap_from_op(self, operation=None, cmap=None, cname=None) : """ Functional assignment of a color from a color map. Face coordinates are used to calculate a scalar which is then used to assign face colors from a colormap. Parameters ---------- operation : function object, default : None Function that takes one argument, a 3xN Numpy array of native coordinates. The function returns a Numpy array of scalar values. If function is None, function will map in the z-direction or r-direction, dependent on the native coordinates. cmap : str or Colormap, optional A Colormap instance or registered colormap name. If not assigned, the surface Colormap is used. The colormap maps the function return values to colors. cname : string, optional, default: None or op function name. Descriptive identifier for values indicated by color. Returns ------- self : surface object """ dftDirName = None if operation is None : opDir = 2 dftDirName = 'Z-direction' if self.coorType == _COORSYS["SPHERICAL"] : opDir = 0 if self.coorType == _COORSYS["CYLINDRICAL"] : opDir = 0 if opDir == 0 : dftDirName = 'R-direction' operation = lambda c : c[opDir] if cmap is not None : if isinstance(cmap,str) : self.set_cmap(cm.get_cmap(cmap)) else : self.set_cmap(cmap) cmap = self.get_cmap() verts = self.vertexCoor[ self.fvIndices ] faceCenterCoor = self._get_face_centers(verts) xyz = np.transpose(faceCenterCoor) # ..... abc = self.coor_convert(xyz) v = np.array(operation(abc)) self.faceValues = v vmin, vmax = v.min(), v.max() norm = colors.Normalize(vmin=v.min(),vmax=v.max()) colorMap = cmap(norm(v)) # FutDev: set_color used instead of set_facecolor to eliminate 'gaps' between faces, # however, set edgecolor required after illumination if edges are to be displayed. # HOWEVER !!!! if colorMap has a alpha<1, these don't register with the edges. self.set_color(colorMap) self.isSurfaceColored = True self._bounds['vlim'] = [ vmin, vmax ] #cname = _getFunctionName(operation,cname) trialName = None if cname is None : if dftDirName is not None : trialName = dftDirName else : trialName = _getFunctionName(operation,None) else: trialName = cname self.valuesName = trialName # ======================================================================== self._map_cmap_from_op_vert(operation, cmap) # ======================================================================== return self
[docs] def map_geom_from_datagrid(self,datagrid, scale=1.0, viewport=None, name=None ) : """ Append surface vertices proportional to 2D datagrid surface. Datagrid values are normalized in the range 0 to 1. This method is not available for composite surfaces. The entire datagrid domain is applied to the geometric operation. Parameters ---------- datagrid : 2D array scale : number, optional, default: 1 viewport : 4D array-like, optional, default: None Viewport defines the subdomain of the surface onto which the datagrid is mapped. The subdomain is set by normalized native surface coordinates in a 4D array. The entire surface is geometrically mapped for the default of None. name : string, optional, default: None Descriptive identifier for the geometry. Returns ------- self : surface object """ if self._isCompositeSurface : warnings.warn('Datagrid operation not available for combined shapes.') return self if self._surfaceShapeChanged : #warnings.warn('Datagrid operation may be anomalous after shape modification.') warnings.warn('Datagrid operation not available after shape modification.') return self a,b,inViewport = self._viewportCoor(self.vertexCoor,viewport) data = datagrid dmax = np.amax(datagrid) dmin = np.amin(datagrid) delta = dmax-dmin data = (datagrid - dmin)/delta xd = np.linspace(0, 1, data.shape[0] ) yd = np.linspace(0, 1, data.shape[1] ) g = interpolate.interp2d(yd, xd, data, kind='cubic') # FutDev: optimize efficiency (?). d = [] for ab in np.transpose([a,b]) : d.append( g(ab[0],ab[1])[0] ) d = np.where(inViewport,d,np.zeros(len(d))) d = d[:, np.newaxis] # FutDev: 'fVal' only applicable for initial subclassed # surfaces to get surface normals at a vertex. # Future work: to make this generic for any surface. fVal = np.array([0.,0.,1.]) if self.disp_Vector is not None : fVal = self.vertexCoor*self.disp_Vector v = self.vertexCoor + scale*d*fVal verts = v[self.fvIndices] self.vertexCoor = v self._set_geometric_bounds() self.set_verts( verts ) self._surfaceShapeChanged = True if name is not None: self._geomName = name self.phongNorms = None # update for v_1.3.0 return self
[docs] def map_geom_from_image(self, fname, scale=1.0, viewport=None, cref='v', hzero=0, name=None ) : """ Append surface vertices proportional to image color component values. For planar and polar coordinates, the z-coordinate is appended. For cylindrical and spherical coordinates, the r-coordinate is appended. This method is not available for composite surfaces. The entire image domain is applied to the geometric operation. Parameters ---------- fname : str or file-like The image file to read: a filename, a URL or a file-like object opened in read-binary mode. Currently restricted to PNG format. scale : number cref : {'r','g','b','h','s','v'}, optional, default: 'v' Sets which color tuple value to use for geometry mapping. Note: only the first character of the string is evaluated. hzero : number or None, optional, default: 0 Argument is used if cref is set to 'h'. The hzero magnitude indicates the Hue value for a zero coordinate diplacement. The hzero sign (positive or negative), indicates the direction of increasing value in the appended coordinate with hue value. Range of hzero is [-1,1]. viewport : 4D array-like, optional, default: None Viewport defines the subdomain of the surface onto which the image is mapped. The subdomain is set by normalized native surface coordinates in a 4D array. The entire surface is geometrically mapped for the default of None. name : string, optional, default: None Descriptive identifier for the geometry. Returns ------- self : surface object """ # .............................................................. def getIndex(cref) : cref = cref[0].lower() valIndex = 'rgbhsv'.find(cref) if valIndex < 0 : raise ValueError('Invalid cref argument: ' + str(cref)) isHSV = False if valIndex > 2 : valIndex -= 3 isHSV = True return valIndex, isHSV # .............................................................. if self._isCompositeSurface : warnings.warn('Image operation not available for combined shapes.') return self if self._surfaceShapeChanged : warnings.warn('Image operation may be anomalous after shape modification.') if (hzero<-1) or (hzero>1) : raise ValueError('hzero must be between -1 and 1, found {}'.format(hzero)) ho = (1.0 -hzero) hsgn = np.sign(hzero) h_ref = lambda h : np.mod( ho + hsgn*h , 1.0) # ................... a,b,inViewport = self._viewportCoor(self.vertexCoor,viewport) img = image.imread(fname) height, width = np.subtract(img.shape[:2],1) x_index = ( (1-b)*height ).astype(int) y_index = ( a*width ).astype(int) valIndex, isHSV = getIndex(cref) rgbcolor = img[x_index,y_index] if isHSV : hsvcolor = colors.rgb_to_hsv(rgbcolor) hsvcolor = np.transpose(hsvcolor) if valIndex==0 : d= h_ref(hsvcolor[0]) else : d = hsvcolor[valIndex] else : rgbcolor = np.transpose(rgbcolor) d = rgbcolor[valIndex] d = np.where(inViewport,d,np.zeros(len(d))) d = d[:, np.newaxis] # FutDev: 'fVal' only applicable for initial subclassed surfaces # surfaces to get surface normals at a vertex. # Future work: to make this generic for any surface. fVal = np.array([0.,0.,1.]) if self.disp_Vector is not None : fVal = self.vertexCoor*self.disp_Vector v = self.vertexCoor + scale*d*fVal verts = v[self.fvIndices] self.vertexCoor = v self._set_geometric_bounds() self.set_verts( verts ) self._surfaceShapeChanged = True if name is not None: self._geomName = name self.phongNorms = None # update for v_1.3.0 return self
[docs] def map_geom_from_op(self, operation, returnxyz=False, name=None) : """ Functional transformation of surface vertex coordinates. This method is not available for composite surfaces. Parameters ---------- operation : function object Coordinate mapping function that takes one argument, a 3xN Numpy array of native coordinates. The function returns a 3xN array of coordinates. returnxyz : bool { True, False }, optional, default: False By default, native coordinates are returned by the operation function. If set True, the operation returns xyz Cartesian coordinates. name : string, optional, default: None or op function name. Descriptive identifier for the geometry. Returns ------- self : surface object """ return self._map_geom_from_op( operation, returnxyz, name, check=True)
def _map_geom_from_op(self, operation, returnxyz, name, check) : # used by domain with check=False to allow composites scaling. # otherwise, not allowed by map_geom_from_op method. if self._isCompositeSurface and check : warnings.warn('Map operation MUST use xyz coordinates for combined shapes.') #warnings.warn('Map operation not available for combined shapes.') #return self v = self._transformVector(self.vertexCoor, operation, returnxyz) verts = v[self.fvIndices] self.vertexCoor = v self._set_geometric_bounds() self.set_verts( verts ) self._surfaceShapeChanged = True if name is None : name = self._geomName name = _getFunctionName(operation,name) if name is not None: self._geomName = name self.phongNorms = None # update for v_1.3.0 return self
[docs] def clip(self, operation, usexyz=False) : """ Remove faces from the surface based on position. Parameters ---------- operation : function object Function that takes one argument, a 3xN Numpy array of coordinates. The function returns a bool { True, False } indicating if the face, at the face centered coordinate, is to be retained (True), otherwise, the face is removed from the surface (False). usexyz : bool { True, False }, default: False If True, face centers are passed to the operation function in xyz coordinates. If False, face centers are passed in native coordinates. Returns ------- self : surface object """ # Clip operation MUST use xyz coordinates for combined shapes. # since native coordinate may be undefined. if self._isCompositeSurface : usexyz = True verts = self.vertexCoor[ self.fvIndices ] faceCenterCoor = self._get_face_centers(verts) self._postProc_surfaceColors() orig_colors = self._dfacecolors # update for v_1.2.0 xyz = np.transpose(faceCenterCoor) if usexyz is False : xyz = self.coor_convert(xyz) abc = xyz shouldKeep = np.array(operation(abc)) if np.any(shouldKeep) is None : warnings.warn('WARNING: Clipping resulted in no faces found, surface return unclipped.') return self fvi = self.fvIndices[shouldKeep] fcol = orig_colors[shouldKeep] # this permits multiple clips without changing original, so restore is available. if self.restoredClip is None : self.restoredClip = { 'faces' : self.fvIndices, 'colors' : orig_colors } fvi = np.array(fvi) self.fvIndices = fvi self.set_color(fcol) v = self.vertexCoor vfvi = v[fvi] self.set_verts( vfvi ) clipVerts = np.reshape(vfvi,(-1,3)) _set_geomBounds( clipVerts,self.bounds ) self._set_index_relations() self.phongNorms = None # update for v_1.3.0 return self
[docs] def clip_alpha(self,alphaCut,useval=False) : """ Remove faces from the surface based on alpha transparency. Parameters ---------- alphaCut : scalar If the face color alpha is greater than alphaCut, the face is retained, otherwise, the face is removed from the surface. useval : bool { True, False }, default: False If True, clipping based on color HSV value. Otherwise, clipping is based on the color alpha value. Returns ------- self : surface object """ self._postProc_surfaceColors() orig_colors = self._dfacecolors # update for v_1.2.0 if useval : orig_hsv = colors.rgb_to_hsv(orig_colors[:,:3]) alphas = np.ravel( orig_hsv[:,2:3] ) else : alphas = np.ravel( orig_colors[:,3:4] ) cuts = np.full(len(alphas),alphaCut) shouldKeep = alphas>cuts if np.any(shouldKeep) is None : warnings.warn('WARNING: Alpha clipping resulted in no faces found, surface return unclipped.') return self fvi = self.fvIndices[shouldKeep] fcol = orig_colors[shouldKeep] # this permits multiple clips without changing original, so restore is available. if self.restoredClip is None : self.restoredClip = { 'faces' : self.fvIndices, 'colors' : orig_colors } fvi = np.array(fvi) self.fvIndices = fvi self.set_color(fcol) v = self.vertexCoor vfvi = v[fvi] self.set_verts( vfvi ) clipVerts = np.reshape(vfvi,(-1,3)) _set_geomBounds( clipVerts,self.bounds ) self._set_index_relations() self.phongNorms = None # update for v_1.3.0 return self
[docs] def clip_plane(self,dist,**kargs) : """ Remove faces from the surface based on a clip surface. Parameters ---------- dist : float Distance from the origin to the clip surface, along the direction vector. direction : array-like, optional, default: [0,0,1] A xyz vector normal to the intersection plane for a planar clip surface or axial direction of a cylinder for a cylindrical clip surface. coor : integer or string indicating the type of clip surface: 0, p, P, xyz,planar - planar (default) 1, c, C, cylinder,pplar,cylindrical - cylinder 2, s, S, sphere,spherical - sphere 3, x, X - y-z plane 4, y, Y - x-z plane 5, z, Z - x-y plane Returns ------- self : surface object """ # ........................................................... def clip_op(xyz,dist,coor,direction) : abc = np.transpose(xyz) dotprod = _coor_dotprod(direction,coor,abc) if coor > 0 : if dist < 0 : shouldclip = np.greater(dotprod,np.full(len(dotprod),-dist)) else : shouldclip = np.less(dotprod,np.full(len(dotprod),dist)) else : shouldclip = np.less(dotprod,np.full(len(dotprod),dist)) return shouldclip # ........................................................... dirDft = { 'direction': [0,0,1.0] } kargDft = { **_COOR_KWARGS, **dirDft } KW = KWprocessor( kargDft, 'clip_plane', **kargs) coor = KW.getVal('coor') direction = KW.getVal('direction') if coor==3 : coor, direction = 0, [ 1,0,0 ] if coor==4 : coor, direction = 0, [ 0,1,0 ] if coor==5 : coor, direction = 0, [ 0,0,1 ] return self.clip( lambda xyz : clip_op(xyz,dist,coor,direction),True)
[docs] def clip_normals(self,direction=None) : """ Remove faces from the surface based on a direction relative to face normals. Parameters ---------- direction : array-like or 3Daxis, optional, default: (30,-60) The vector direction for viewing (elev,azim) or 3Daxis. Returns ------- self : surface object """ if direction is not None: temp = direction try : # check if direction holds the elev, azim (ie. 3D axis) elev,azim = temp.elev, temp.azim direction = np.array( elev_azim_2vector(elev,azim) ) except : direction = temp if len(direction) != 3 : raise ValueError('Clip from normals direction must be a array of length 3, or a Matplotlib Axes3D object.') else : direction = np.array( elev_azim_2vector( *_DFT_VIEW ) ) verts = self.vertexCoor[ self.fvIndices ] faceNormalCoor = self._get_face_normals(verts) self._postProc_surfaceColors() orig_colors = self._dfacecolors # update for v_1.2.0 shouldKeep = np.dot(faceNormalCoor,direction) > 0.0 if np.any(shouldKeep) is None : warnings.warn('WARNING: Normal clipping resulted in no faces found, surface return unclipped.') return self fvi = self.fvIndices[shouldKeep] fcol = orig_colors[shouldKeep] # this permits multiple clips without changing original, so restore is available. if self.restoredClip is None : self.restoredClip = { 'faces' : self.fvIndices, 'colors' : orig_colors } fvi = np.array(fvi) self.fvIndices = fvi self.set_color(fcol) v = self.vertexCoor vfvi = v[fvi] self.set_verts( vfvi ) clipVerts = np.reshape(vfvi,(-1,3)) _set_geomBounds( clipVerts,self.bounds ) self._set_index_relations() self.phongNorms = None # update for v_1.3.0 return self
def _restore_from_clip(self) : """ Reset face and colors prior to any clip operation. (in development) """ # FutDev: this method needs thorough testing. # probably doesn't work as is. if self.restoredClip is None : warnings.warn('There is no clipped surface to restore.') return self fvi = self.restoredClip['faces'] fcol = self.restoredClip['colors'] self.restoredClip = None self.fvIndices = fvi self.set_color(fcol) v = self.vertexCoor self.set_verts(v[ np.array(fvi) ]) self._set_geometric_bounds() # reset to original bounds... return self
[docs] def set_surface_alpha(self, alpha, constant=False, adjustlw=True) : """ Adjust the face color alpha values and linewidth of the surface. Parameters ---------- alpha : scalar Alpha is in the range 0 to 1. constant : bool { True, False }, optional, False If False, face color values are multiplied by alpha. If True, all face colors alpha channels are assigned to alpha. adjustlw : bool { True, False }, optional, True If True, the surface linewidth will be set to make the edges visually transparent with the faces for Matplotlib rendering. Returns ------- self : surface object """ if (alpha<0.0) or (alpha>1.0) : raise ValueError('surface alpha must be between 0 and 1, found {}'.format(alpha)) self._postProc_surfaceColors() orig_colors = self._dfacecolors # update for v_1.2.0 if constant : np.put_along_axis(orig_colors, np.array([[3]]), alpha, axis=1) colorMap = orig_colors else: colorMap = np.multiply(orig_colors,np.array([1.0,1.0,1.0,alpha])) self.set_color(colorMap) if adjustlw : # heuristic approach to hiding face edges. BANDAID lw = 0.0063*np.exp(4.2*alpha) self.set_linewidth(lw) return self
def _get_face_normals_from_phongNorms(self) : # called from shade and hilite # update for v_1.3.0 # note: lint will flag a problem : 'self.phongNorms is unsubscriptable'. # self.phongNorms is set to None as a flag, but on triangulation, # it is assigned an array (so no problem ;-). vNorms = self.phongNorms[ np.array(self.fvIndices) ] sumNorms = np.sum(vNorms,axis=1) lnorm = np.linalg.norm(sumNorms,axis=1) lnorm3 = np.empty([3,lnorm.shape[0]]) lnorm3[:] = lnorm norm = np.divide( sumNorms, lnorm3.T) return norm
[docs] def shade(self, depth=0, direction=None, contrast=None, isAbs=False, ax=None, rview=False, flat=True) : """ Reduce surface HSV color Value based on face normals. The dot product of face normals with the illumination direction is used to adjust face HSV color value. Parameters ---------- depth : scalar, optional, default: 0 Minimum color value of shaded surface faces. Depth value ranges from 0 to 1. direction : array-like, optional, default: (1,0,1) A xyz vector pointing to the illumination source. contrast : scalar, optional, default: 1 Shading contrast adjustment from low to high with a value of 1 for linear variations with the normal direction. Contrast value ranges from 0.1 to 3. isAbs : bool, optional, default: False If True, the absolute value of the dot product is is used to determine color value. ax : Matplotlib 3D axes. rview : boolean, default: False If True, direction is relative to the view, otherwise, relative to the axes. flat : boolean, default: True If False, smooth flat triangulated faces. Returns ------- self : surface object """ if direction is None : direction = _ILLUM viewdir,ausf = None,None if ax is not None: ausf = _axisUSF(ax) viewdir = elev_azim_2vector(ax.elev,ax.azim) if rview : direction = rtv(direction,ax.elev,ax.azim) if np.any( (depth<0) | (depth>1) ) : raise ValueError('depth values, {}, must be between 0 and 1'.format(depth)) if contrast is not None: if (contrast<0.1 or contrast>3) : raise ValueError('contrast must be between 0.1 and 3. , found {}'.format(contrast)) # for triangulation smoothing # update for v_1.3.0 if not (self.phongNorms is None or flat) : norms = self._get_face_normals_from_phongNorms() else : verts = self.vertexCoor[ np.array(self.fvIndices) ] norms = self._get_face_normals(verts,ausf) unitDirection = np.divide( direction, np.linalg.norm(direction) ) dprod = np.dot( norms, unitDirection ) if viewdir is not None : vdot = np.dot(norms,viewdir) dprod = np.where(np.less(vdot,[0.0]),-dprod,dprod) # 0<d<1, heuristic function for # normalized domain of effective dot product, d=f(dprod) if isAbs : d = 1 - np.abs(dprod) else : d = ( dprod + 1 )/2 d = np.abs(d) # to insure positive values near zero if contrast is not None : d = 0.5*( 1 + np.sign(dprod)*np.power( np.abs(dprod) , 1/contrast) ) # extract HSV, leaving alpha unchanged. self._postProc_surfaceColors() orig_colors = self._dfacecolors # update for v_1.2.0 alphas = orig_colors[:,3:4] fc_less_alpha = orig_colors[:,:3] hsv_vals = cm.colors.rgb_to_hsv(fc_less_alpha) # adjust color value with multiplier. cvm = ((1-depth)*d + depth*np.ones(len(d)))[:, np.newaxis] hsv_vals[:,2] = (cvm*hsv_vals[:,2:3])[:,0] rgb_vals = cm.colors.hsv_to_rgb(hsv_vals) shade_colors = np.concatenate((rgb_vals, alphas), axis=1) self.set_color(shade_colors) # NOTE: this will also set appropriate edge colors. return self
[docs] def hilite(self, height=1, direction=None, focus=None, ax=None, rview=False, flat=True) : """ Increase surface HSV color Value and reduce Saturation, based on face normals. The dot product of face normals with the illumination direction is used to adjust face HSV color value and saturation. Faces having normals with a negative component in the direction of illumination are not affected. The reduction of color saturation is proportional to the increase in color value. Parameters ---------- height : scalar, optional, default: 1 Maximum color value of highlighted surface faces. Height value ranges from 0 to 1. direction : array-like, optional, default: (1,0,1) A xyz vector pointing to the illumination source. focus : scalar, optional, default: 1 Highlighting focus adjustment from low to high with a value of 1 for quadratic variations with the normal direction. Focus value ranges from 0.1 to 3. ax : Matplotlib 3D axes. rview : boolean, default: False If True, direction is relative to the view, otherwise, relative to the axes. flat : boolean, default: True If False, smooth flat triangulated faces. Returns ------- self : surface object """ if direction is None : direction = _ILLUM viewdir,ausf = None,None if ax is not None: ausf = _axisUSF(ax) viewdir = elev_azim_2vector(ax.elev,ax.azim) if rview : direction = rtv(direction,ax.elev,ax.azim) if np.any( (height<0) | (height>1) ) : raise ValueError('height values, {}, must be between 0 and 1'.format(height)) if focus is not None: if (focus<0.1 or focus>3) : raise ValueError('focus must be between 0.1 and 3. , found {}'.format(focus)) # for triangulation smoothing # update for v_1.3.0 if not (self.phongNorms is None or flat) : norms = self._get_face_normals_from_phongNorms() else : verts = self.vertexCoor[ np.array(self.fvIndices) ] norms = self._get_face_normals(verts,ausf) unitDirection = np.divide( direction, np.linalg.norm(direction) ) dprod = np.dot( norms, unitDirection ) if viewdir is not None : vdot = np.dot(norms,viewdir) dprod = np.where(np.less(vdot,[0.0]),-dprod,dprod) # 0<d<1, heuristic function for # normalized domain of effective dot product, d=f(dprod) d = np.where(dprod<0 , 0, dprod) if focus is None : focus = 1 d0 = np.power(focus,1/2)/2.0 y = (d-d0)/(1-d0) d = np.where(y<0 , 0, y) d = np.power(d,1.0+2*focus) # extract HSV, leaving alpha unchanged. self._postProc_surfaceColors() orig_colors = self._dfacecolors # update for v_1.2.0 alphas = orig_colors[:,3:4] fc_less_alpha = orig_colors[:,:3] hsv_vals = cm.colors.rgb_to_hsv(fc_less_alpha) # adjust color saturation and value, linear with normalized domain. hd = height*d omhd = (np.ones( len(norms) ) - hd) omhd = omhd[:,np.newaxis] hsv_vals[:,1] = (omhd*hsv_vals[:,1:2])[:,0] hsv_vals[:,2] = (omhd*hsv_vals[:,2:3])[:,0] hsv_vals[:,2] += hd rgb_vals = cm.colors.hsv_to_rgb(hsv_vals) hilite_colors = np.concatenate((rgb_vals, alphas), axis=1) self.set_color(hilite_colors) return self
[docs] def fade(self,depth=0,elev=None,azim=None,ax=None) : """ Reduce surface face opacity based on face position relative to the view orientation. Parameters ---------- depth : scalar, optional, default: 0 Minimum opacity to 1 for face opacity from back to front face center position. Depth value ranges from 0 to 1. elev : scalar, optional, default: 30 Elevation of the axes view. azim : scalar, optional, default: -60 Azimuth of the axes view. ax: Matplotlib 3D axes. If not None, elev and azim are taken from the ax. Returns ------- self : surface object """ if ax is None : if elev is None: elev = _DFT_VIEW[0] if azim is None: azim = _DFT_VIEW[1] else : elev = ax.elev azim = ax.azim direction = elev_azim_2vector(elev, azim) unitDirection = np.divide( direction, np.linalg.norm(direction) ) if np.any( (depth<0) or (depth>1) ) : raise ValueError('depth values, {}, must be between 0 and 1'.format(depth)) verts = self.vertexCoor[ np.array(self.fvIndices) ] faceCenterCoor = self._get_face_centers(verts) self._postProc_surfaceColors() colors = self._dfacecolors # update for v_1.2.0 if len(faceCenterCoor) == 1 : return self # fade not available for single face line dtprod = np.dot(faceCenterCoor,unitDirection) dprange = np.amax(dtprod)-np.amin(dtprod) norm_dp = (dtprod - np.amin(dtprod))/dprange fadex = (1-depth)*norm_dp + depth orig_colors = colors fc_less_alpha = orig_colors[:,:3] fadex = fadex*orig_colors[:,3] # update for v_1.2.0 faded_colors = np.concatenate((fc_less_alpha, fadex[:,np.newaxis] ), axis=1) self.set_color(faded_colors) # heuristic approach to hiding face edges. BANDAID update for v_1.2.0 alpha = (depth+1)/2 # take the midpoint of the gradient. lw = 0.0063*np.exp(4.2*alpha) self.set_linewidth(lw) return self
[docs] def transform(self, rotate=None, scale=1.0, translate=[0,0,0] ) : """ Linear transformation of the surface object. Parameters ---------- rotate : array-like, optional, default: None A 3 by 3 rotation matrix. scale : 3D array or scalar, optional, default: 1.0 Multipliers of the object coordinates, in xyz coordinates. If a single scalar, three multipliers are the same value. translate : array-like, optional, default: [0,0,0] A xyz translation vector of object origin. Returns ------- self : surface object """ if rotate is None : rotate = np.identity(3) if isinstance(scale,(int,float)) : scale = np.full(3,scale).astype(float) #..................................... def isOrth(M) : M = np.array(M) tol = 0.00001 prod = np.dot(M,M.T) np.fill_diagonal(prod,0.0) return not (np.abs(prod)>tol).any() #..................................... trans = lambda V,S,T : np.add( np.dot( np.multiply(V,S),rotate) ,T) #..................................... if not isOrth(rotate) : warnings.warn('Transform rotate matrix is NOT Orthoginal.') v = trans( self.vertexCoor, scale, translate ) if self.phongNorms is not None : # update for v_1.3.0 pV = trans( self.phongNorms, scale, translate ) self.phongNorms = pV if self.vector_field is not None : nScal = np.divide(scale, np.linalg.norm(scale) ) self.vector_field = trans( self.vector_field, nScal, [0,0,0] ) self.vertexCoor = v self._set_geometric_bounds() self.set_verts( v[ np.array(self.fvIndices) ] ) self._surfaceShapeChanged = True self.scale = scale self.translation = translate self.rotation = rotate return self
[docs] def get_transformAxis(self, **kargs) : """ Line3DCollection of the 'last' transformed surface coordinate axis. Parameters ---------- lenmult : scalar or 3D array, optional, default: 1 Scalar multiplier of the three coordinate axis. width : scalar, optional, default: 1.5 Line width of the coordinate axis. color : a Color or 3D array of Colors, optional, default: ['r','g','b'] negaxis : boolean, optional, default: False If True, include the negative axis, otherwise axis start at origin. Returns ------- Line3DCollection object """ kargDft = { 'lenmult':1.0, 'width':1.5, 'color':['r','g','b'], 'negaxis':False } KW = KWprocessor(kargDft,'get_transformAxis',**kargs) lenmult = KW.getVal('lenmult') width = KW.getVal('width') color = KW.getVal('color') negaxis = KW.getVal('negaxis') selfProp = { 'scale' : self.scale, 'rotation' : self.rotation, 'translation' : self.translation } return _transformAxis(selfProp,lenmult, width, color, negaxis)
[docs] def map_geom_from_svd(self, data, pc=None ) : """ Transform surface geometry based on a PCA analysis of a data set. Result values contained in the surface property svd_dict. Parameters ---------- data : N x 3 array of x,y,z data coordinates. pc : scalar, optional, default: None Sets the minimum size of the surface based on the relative number of data. Value range from 0 to 1, with 1 for 100%. If None, one standard deviation of the data sets the surface size. Returns ------- self : surface object """ if self._isCompositeSurface : warnings.warn('Data operation not available for combined shapes.') return self data_mean = data.mean(axis=0) data_centered = data - data_mean Ua, sa, Vta = np.linalg.svd(data_centered) dataAve = np.mean(sa) scale = sa/dataAve tranData = np.divide(np.dot(data_centered,Vta.T),scale ) disArr=np.linalg.norm(tranData,axis=1) disArrSum = np.linalg.norm(disArr) lendisArr = len(disArr) refLen = disArrSum/np.sqrt(lendisArr) s=refLen disArr = np.divide(disArr,refLen) if pc is not None: dlen = len(disArr) dindex = int(dlen*pc) if dindex >= dlen : dindex = dlen - 1 if dindex < 0 : dindex = 0 s = np.sort(disArr)[dindex] disArr = disArr/s s = s*refLen self.transform(Vta,s*scale,data_mean) self._svd_dict = { 'disarr': disArr, 'sigma': refLen, 'trans': [Vta,s*scale,data_mean] } self.phongNorms = None # update for v_1.3.0 return self
[docs] def contourLines(self,*dist,**kargs) : """ ColorLine3DCollection contour lines on the surface. Parameters ---------- dist : floats Distances from the origin to the surface of intersection, along the direction vector. direction : array-like, optional, default: [0,0,1] A xyz vector normal to the intersection planes for planar contours or axial direction of cylinders for cylindrical contours. name : string identifier (default None) color : color (default is the surface colors) coor : integer or string indicating the type of contour surface: 0, p, P, xyz,planar - planar (default, planar & polar) 1, c, C, cylinder,pplar,cylindrical - cylinder (default, cylindrical) 2, s, S, sphere,spherical - sphere (default, spherical) 3, x, X - y-z plane 4, y, Y - x-z plane 5, z, Z - x-y plane Returns ------- self : line object """ extDft = { 'direction':[0,0,1.0], 'name': None, 'color': None } kargDft = { **_COOR_KWARGS, **extDft } # reset default 'coor' for cylindrical and spherical coor surfaces. coorVal = 0 if self.coorType == _COORSYS["CYLINDRICAL"] : coorVal = 1 if self.coorType == _COORSYS["SPHERICAL"] : coorVal = 2 if coorVal != 0 : temp = copy.deepcopy(_COOR_KWARGS) temp['coor'][0] = [coorVal] kargDft = { **temp, **extDft } KW = KWprocessor(kargDft,'contourLines',**kargs) cIndex = KW.getVal('coor') direction = KW.getVal('direction') name = KW.getVal('name') color = KW.getVal('color') if direction is None : direction = extDft['direction'] if cIndex==3 : cIndex, direction = 0, [ 1,0,0 ] if cIndex==4 : cIndex, direction = 0, [ 0,1,0 ] if cIndex==5 : cIndex, direction = 0, [ 0,0,1 ] dotprod = _coor_dotprod(direction,cIndex,self.vertexCoor) conLines = self._contourLineCollection( dotprod,direction,*dist ) if conLines is None : raise ValueError('No surface.contourLines were found.') if cIndex == 0 : conLines._planeNormal = direction if name is not None : conLines.name = kargs['name'] if color is not None : conLines.set_color(kargs['color']) return conLines
[docs] def contourLineSet(self,numb=2,**kargs) : """ ColorLine3DCollection of evenly spaced contour lines on the surface. Parameters ---------- numb : integer Number of contour lines intersecting a surface. Numb value must be getter than zero. direction : array-like, optional, default: [0,0,1] A xyz vector normal to the intersection planes for planar contours or axial direction of cylinders for cylindrical contours. name : string identifier (default None) color : color (default is the surface colors) coor : integer or string indicating the type of contour surface: 0, p, P, xyz,planar - planar (default, planar & polar) 1, c, C, cylinder,pplar,cylindrical - cylinder (default, cylindrical) 2, s, S, sphere,spherical - sphere (default, spherical) 3, x, X - y-z plane 4, y, Y - x-z plane 5, z, Z - x-y plane Returns ------- self : line object """ extDft = { 'direction':[0,0,1.0], 'name': None, 'color': None } kargDft = { **_COOR_KWARGS, **extDft } # reset default 'coor' for cylindrical and spherical coor surfaces. coorVal = 0 if self.coorType == _COORSYS["CYLINDRICAL"] : coorVal = 1 if self.coorType == _COORSYS["SPHERICAL"] : coorVal = 2 if coorVal != 0 : temp = copy.deepcopy(_COOR_KWARGS) temp['coor'][0] = [coorVal] kargDft = { **temp, **extDft } KW = KWprocessor(kargDft,'contourLineSet',**kargs) cIndex = KW.getVal('coor') direction = KW.getVal('direction') name = KW.getVal('name') color = KW.getVal('color') dotprod = _coor_dotprod(direction,cIndex,self.vertexCoor) maxd = np.amax(dotprod) mind = np.amin(dotprod) convals = np.linspace(mind,maxd,numb+2)[1:numb+1] convals = tuple(convals) conLines = self._contourLineCollection( dotprod,direction,convals ) if cIndex == 0 : conLines._planeNormal = direction if name is not None : conLines.name = kargs['name'] if color is not None : conLines.set_color(kargs['color']) return conLines
[docs] def evert(self) : """ Reverse direction of face normals. """ self.fvIndices = np.flip(self.fvIndices, axis=1) if self.phongNorms is not None : # update for v_1.3.0 self.phongNorms = -1.0 * self.phongNorms return self
[docs] def domain(self, xlim=None, ylim=None, zlim=None) : """ Set the domain of the surface. Used for setting the domain of the base. Parameters ---------- xlim, ylim, zlim : 2d arrays Minimum and maximum values of the arrays are used to scale and translate the surface from an intial domains of [ -1, 1 ] If set to None, the domains are unchanged. Returns ------- self : surface object """ def setDomain(xyz, mx,bx, my,by, mz,bz) : x,y,z = xyz X = mx*x + bx Y = my*y + by Z = mz*z + bz return X,Y,Z def getCont( lims ) : m, b = 1.0, 0.0 if lims is not None : # allow single bndry to be input as a float if isinstance(lims, (int,float)) : lims = [-lims,lims] m, b = 0.5*(lims[1]-lims[0]) , 0.5*(lims[1]+lims[0]) return m, b mx, bx = getCont(xlim) my, by = getCont(ylim) mz, bz = getCont(zlim) self._map_geom_from_op(lambda A : setDomain(A, mx,bx, my,by, mz,bz),False,None,False) return self
[docs]class CubicSurface(Surface3DCollection) : """ Cubic 3D surface in Cartesian coordinates with rectangular faces. Methods are inherited from the Surface3DCollection. """ @staticmethod def _get_cubic_surface_dictionary() : """ Constructs the dictionary of base planar surface geometries. """ surfacesDictionary = {} baseVert = [] baseVert = [ [ 1,-1, -1], [ 1, 1, -1], [-1, 1, -1], [-1,-1, -1], [ 1,-1, 1], [ 1, 1, 1], [-1, 1, 1], [-1,-1, 1] ] cubeFaces=( [0,1,5,4], [1,2,6,5], [2,3,7,6], [3,0,4,7], [4,5,6,7], [0,3,2,1] ) Fc = len(cubeFaces) cube = { 'baseFaceIndices' : cubeFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fc,0,2] } surfacesDictionary['cubic'] = cube return surfacesDictionary _base_surfaces = _get_cubic_surface_dictionary.__func__() _default_base = 'cubic' # default assignment is indicated in __init Docstring. @classmethod def _Xfev(cls,rez, basetype=None) : """ Calculates the number of faces, edges, and vertices of a CubicSurface. Parameters ---------- rez : integer Number of recursive subdivisions of a triangulated base faces. basetype : None Placeholder argument has no effect. Returns ------- list: the number of faces, edges, and vertices. """ F = 6*(4**rez) return [ F, 2*F, F+2 ] def __init__(self, rez=0, name=None, **kwargs): """ Create a cubic surface of 2 x 2 x 2 units. Parameters ---------- rez : integer, optional, default: 0 Number of recursive subdivisions of the rectangulated base faces. Rez values range from 0 to 7. name : string, optional, default: None. Descriptive identifier for the geometry. Raises ------ ValueError If rez is not an integer in range 0 to 7. Other Parameters ---------------- **kwargs All other parameters are passed on to 's3dlib.surface.Surface3DCollection'. """ # ................................................................ def midVFun(vectA, vectB) : """Mid-point between two points in the xy plane.""" mid = np.add(vectA,vectB) mid = np.multiply(0.5,mid) return mid # ................................................................ if not isinstance(rez, int) : raise ValueError('Incorrect rez type, must ba of type int') if (rez<0) or (rez>_MAXREZ) : raise ValueError('Incorrest rez of {}, must be an int, 0 <= rez <= {}'.format(rez,_MAXREZ)) basetype = self._default_base baseSurfObj = self._base_surfaces[basetype] baseVcoor = baseSurfObj['baseVerticesCoor'] baseFaceVertexIndices =baseSurfObj['baseFaceIndices'] indexObj,vertexCoor = self._rectangulateBase(rez,baseVcoor,baseFaceVertexIndices, midVFun) faceIndices = indexObj['face'] super().__init__(vertexCoor, faceIndices, name, **kwargs) self.coorType = _COORSYS["XYZ"] self._normal_scale = _normLength( len(faceIndices), 0.61, 1.50 ) self._rez = rez self._basetype = basetype return
[docs] def domain(self, xlim, ylim=None, zlim=None) : """ Set the domain of the cubic surface. Used for setting the min,max values of the base cube. Note: this CubicSurface method uses an alternative list of arguments from the base class domain method. Using the xlim arg abbreviates setting identical domains for all three axes. Parameters ---------- xlim : a number, list or array, default: 1 The domain of the function evaluation. For a number, n, the x,y,z axes domains will be [-n,n]. For a 1-dimensional 2-element list, [a,b] will be assigned the domain for all 3 axes. Using a list of list (array), as [ [a,b],[c,d],[e,f] ], the domain is assigned individually for each of the three coordinate axes. Must be a 2D array if ylim or zlim is set. ylim, zlim : 2D array, default: None Set the ylim and/or zlim. Returns ------- self : CubicSurface object """ # the following allows alternative 'arg list' used in super. test = ylim is None and zlim is None if not test: super().domain(xlim,ylim,zlim) return self domain = _interpret_domain(xlim) # update for v_1.3.0 scl = lambda min,max : (max-min)/2 trn = lambda min,max : (max+min)/2 scale = [ scl(d[0],d[1]) for d in domain ] translate = [ trn(d[0],d[1]) for d in domain ] self.transform(None,scale,translate) return self
[docs]class PlanarSurface(Surface3DCollection) : """ Flat square 3D surface in Cartesian coordinates. Methods are inherited from the Surface3DCollection. Planar surface geometries are created in this subclass. """
[docs] @staticmethod def rand(rez=0,seed=None,name=None,kind=None,**kargs) : """ PlanarSurface with random triangular faces. Parameters ---------- rez : number, optional, default: 0 Number of 'recursive' subdivisions of the triangulated base faces. Rez values range from 0 to 7. seed : integer, optional, default: None An initialization seed for random generation. name : string, optional, default: None. Descriptive identifier for the geometry. kind : string, optional, default: None (reserved, not implemented) Raises ------ ValueError If rez is not an integer in range 0 to 7. If basetype is not recognized for this surface constructor. Other Parameters ---------------- **kwargs All other parameters are passed on to 's3dlib.surface.Surface3DCollection'. Returns ------- surface : PlanarSurface object. """ if not isinstance(rez, (int,float)) : raise ValueError('Incorrect rez type, must be a number') if (rez<0) or (rez>_MAXREZ) : raise ValueError('Incorrest rez of {}, must be an int, 0 <= rez <= {}'.format(rez,_MAXREZ)) if seed is not None : if not isinstance(seed, int) : raise ValueError('Incorrect seed type, must ba of type int') np.random.seed(seed) N = int( 8*(4**rez)/2 + 4*(2**rez) + 1 ) x = 2*np.random.rand( N ) - 1 y = 2*np.random.rand( N ) - 1 xydata = np.array([x,y]).T tess = spatial.Delaunay(xydata) if name is None : name = 'random mesh' surface = PlanarSurface(name=name,**kargs) # set to the class default object # reset verts, faceIndices and edges (retain class coordinate system). verts = np.zeros( (N,3)) verts[:,:2] = tess.points faceIndices = tess.simplices surface.vertexCoor = verts surface.fvIndices = faceIndices surface.evIndices = surface._get_edges_from_faces(faceIndices) surface.vfIndicesList = surface._vertexCommonFaceIndices() surface.efIndicesList = surface._edgeCommonFaceIndices() surface.set_verts( verts[faceIndices] ) surface._set_geometric_bounds() surface._rez = str(rez) if isinstance(rez,int) else '{:.1f}'.format(rez) surface._basetype = 'rand_'+str(seed) return surface
@staticmethod def _get_planar_surfaces_dictionary() : """ Constructs the dictionary of base planar surface geometries. """ surfacesDictionary = {} baseVert = [ [0,0,0], [1,1,0], [-1,1,0], [-1,-1,0], [1,-1,0], [1,0,0], [0,1,0], [-1,0,0], [0,-1,0] ] qVert = [ baseVert[i] for i in range(0,5) ] quadFaces=( [0,1,2],[0,2,3],[0,3,4],[0,4,1] ) octFaces1=( [0,5,6],[0,6,7],[0,7,8],[0,8,5], [5,1,6],[6,2,7],[7,3,8],[8,4,5] ) octFaces2=( [0,5,1],[0,1,6],[0,6,2],[0,2,7], [0,7,3],[0,3,8],[0,8,4],[0,4,5] ) gridFaces=( [0,5,1,6], [0,6,2,7], [0,7,3,8], [0,8,4,5] ) Fq = len(quadFaces) F1 = len(octFaces1) F2 = len(octFaces2) Fc = len(gridFaces) quad = { 'baseFaceIndices' : quadFaces , 'baseVerticesCoor' : qVert, 'fevParam' : [Fq,2,1] } oct1 = { 'baseFaceIndices' : octFaces1 , 'baseVerticesCoor' : baseVert, 'fevParam' : [F1,4,1] } oct2 = { 'baseFaceIndices' : octFaces2 , 'baseVerticesCoor' : baseVert, 'fevParam' : [F2,4,1] } grid = { 'baseFaceIndices' : gridFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fc,0,2] } surfacesDictionary['quad'] = quad surfacesDictionary['oct1'] = oct1 surfacesDictionary['oct2'] = oct2 surfacesDictionary['squ'] = grid # ---------------------------------------------------------- mr = 0.01 MR = mr*np.sqrt(2) qVertS=copy.copy( surfacesDictionary['quad']['baseVerticesCoor'] ) qVertS[0] = [ MR, 0, 0] qVertS.append([ 0, MR, 0]) qVertS.append([-MR, 0, 0]) qVertS.append([0 ,-MR, 0]) baseVertS = copy.copy(surfacesDictionary['oct1']['baseVerticesCoor']) baseVertS[0] = [ mr, mr, 0] baseVertS.append([-mr, mr, 0]) baseVertS.append([-mr,-mr, 0]) baseVertS.append([ mr,-mr, 0]) quadFacesS=( [5,1,2],[6,2,3],[7,3,4],[0,4,1], [1,5,0],[2,6,5],[3,7,6],[4,0,7] ) octFaces1S=( [ 0, 5, 6],[ 9, 6, 7],[10, 7, 8],[11, 8, 5], [ 5, 1, 6],[ 6, 2, 7],[ 7, 3, 8],[ 8, 4, 5], [ 5, 0,11],[ 6, 9, 0],[ 7,10, 9],[ 8,11,10] ) octFaces2S=( [ 0, 5, 1],[ 0, 1, 6],[ 9, 6, 2],[ 9, 2, 7], [10, 7, 3],[10, 3, 8],[11, 8, 4],[11, 4, 5], [ 5, 0,11],[ 6, 9, 0],[ 7,10, 9],[ 8,11,10] ) FqS = len(quadFacesS) F1S = len(octFaces1S) F2S = len(octFaces2S) quadS = { 'baseFaceIndices' : quadFacesS , 'baseVerticesCoor' : qVertS, 'fevParam' : [FqS,2,1] } oct1S = { 'baseFaceIndices' : octFaces1S , 'baseVerticesCoor' : baseVertS, 'fevParam' : [F1S,4,1] } oct2S = { 'baseFaceIndices' : octFaces2S , 'baseVerticesCoor' : baseVertS, 'fevParam' : [F2S,4,1] } surfacesDictionary['quad_c'] = quadS surfacesDictionary['oct1_c'] = oct1S surfacesDictionary['oct2_c'] = oct2S return surfacesDictionary @staticmethod def _midVectorFun(vectA, vectB) : """Mid-point between two points in the xy plane.""" mid = np.add(vectA,vectB) mid = np.multiply(0.5,mid) return mid _base_surfaces = _get_planar_surfaces_dictionary.__func__() _default_base = 'quad' # default assignment is indicated in __init Docstring.
[docs] @classmethod def grid(cls,xdiv, ydiv, basetype='r', minrad=None, name=None, **kwargs) : """ PlanarSurface with X and Y rectangular faces. Parameters ---------- xdiv : integer, required. Number of X divisions. ydiv : integer, required. Number of Y divisions. basetype : {'d','r'}, optional, default: 'r' Starting surface geometries from which the surface is constructed indicating face shape. minrad : placeholder, not used. name : string, optional, default: None. Descriptive identifier. Raises ------ ValueError If xdiv is not an integer greater or equal to 1. If ydiv is not an integer greater or equal to 1. Other Parameters ---------------- **kwargs All other parameters are passed on to 's3dlib.surface.Surface3DCollection'. Returns ------- surface : PlanarSurface object. """ splitSize = None minrad = None # note: reversal of nang, nrad argsuments compared to other inherited class grid methods. nrad, nang = ydiv, xdiv surface = cls._square_net(nrad, nang, basetype,minrad, splitSize, name, **kwargs) surface._postProc_surfaceColors(True) surface._rez = surface._calc_rez( 4 ) return surface
[docs] @classmethod def pntsurf(cls, xyz_points, name=None, **kargs) : """ PlanarSurface from points in Cartesian coordinates. Parameters ---------- xyz_points : array, required. An N X 3, or N X 4, array of N Cartesian coordinate points. For an N X 4 array, the 4th is the vertex scalar value. name : string, optional, default: None. Descriptive identifier for the point surface. Returns ------- surface : PlanarSurface object """ dataPts = np.array(xyz_points) # update for v_1.3.0 dataPts, vVal = Surface3DCollection._extract_vals_from_inputCoor(dataPts) xyzPts = dataPts.T fvIndices = tri.Triangulation(xyzPts[0],xyzPts[1]).triangles base_surface = Surface3DCollection(dataPts, fvIndices, color='tan').shade() faceIndices = base_surface.fvIndices verts = dataPts if name is None : name = 'point surface' surface = PlanarSurface(name=name,**kargs) surface.vertexCoor = verts surface.fvIndices = faceIndices surface.evIndices = surface._get_edges_from_faces(faceIndices) surface.vfIndicesList = surface._vertexCommonFaceIndices() surface.efIndicesList = surface._edgeCommonFaceIndices() surface.set_verts( verts[faceIndices] ) surface._set_geometric_bounds() surface._rez = surface._calc_rez(4) surface._basetype = 'pntsurf' if vVal is not None : surface.set_vertvals(vVal) # update for v_1.3.0 return surface
@classmethod def _Xfev(cls,rez, basetype=None) : """ Calculates the number of faces, edges, and vertices of a PlanarSurface. Parameters ---------- rez : integer Number of recursive subdivisions of a triangulated base faces. basetype : string Starting surface geometries from which the surface would be constructed using recursive subdivisions of the triangular faces. Returns ------- list: the number of faces, edges, and vertices. """ return super()._fev(rez,basetype,cls)
[docs] @staticmethod def meshgrid(X,Y,Z,revnormal=False,grid=False, name=None) : """ PlanarSurface object based on a meshgrid. Parameters ---------- X,Y,Z : N x M arrays Cartesian x,y,z coordinate values. revnormal : { True, False }, boolean, default: False Reverse the face normal directions if True. grid : { True, False }, boolean, default: False If True, polygon faces are rectangular. If False, polygon faces are triangular. name : string, optional, default: None. Descriptive identifier for the mesh surface. Returns ------- surface : PlanarSurface object """ vertCoor = np.reshape(np.stack((X,Y,Z), axis=2 ), (-1,3) ) lenY, lenX = Z.shape mtr = np.reshape( np.arange(lenX*lenY) , (lenY,lenX)) A = mtr[:,0:lenX-1][0:lenY-1,:].flatten() B = A + 1 C = A + lenX +1 D = A + lenX if revnormal : t = B B = D D = t faceIndices = np.stack( (A,B,C,D) ).T if grid : obj = PlanarSurface( basetype='squ') else : obj = PlanarSurface( ) obj.vertexCoor = vertCoor obj.fvIndices = faceIndices obj._normal_scale = _normLength( len(faceIndices), 0, 0, 4 ) obj._set_geometric_bounds() obj._surfaceShapeChanged = True v = obj.vertexCoor fvi = obj.fvIndices if grid : obj.vfIndicesList = obj._vertexCommonFaceIndices() edgeIndices = obj._get_edges_from_faces(obj.fvIndices) obj.evIndices = np.array(edgeIndices) obj.efIndicesList = obj._edgeCommonFaceIndices() obj.set_verts(v[fvi]) if not grid : obj.triangulate() if name is None : name = 'meshgrid' obj.name = name return obj
def __init__(self, rez=0, basetype=None, name=None, **kwargs): """ Create flat square surface of 2 x 2 units. Parameters ---------- rez : integer, optional, default: 0 Number of recursive subdivisions of the triangulated base faces. Rez values range from 0 to 7. basetype : {'quad','oct1','oct2','squ'}, optional, default: 'quad' Starting surface geometries from which the surface is constructed using recursive subdivisions of the faces. name : string, optional, default: None. Descriptive identifier for the geometry. Raises ------ ValueError If rez is not an integer in range 0 to 7. If basetype is not recognized for this surface constructor. Other Parameters ---------------- **kwargs All other parameters are passed on to 's3dlib.surface.Surface3DCollection'. """ if not isinstance(rez, int) : raise ValueError('Incorrect rez type, must ba of type int') if (rez<0) or (rez>_MAXREZ) : raise ValueError('Incorrest rez of {}, must be an int, 0 <= rez <= {}'.format(rez,_MAXREZ)) if basetype is None : basetype = self._default_base if basetype not in self._base_surfaces : raise ValueError('Basetype {} is not recognized. Possible values are: {}'.format(basetype,list(self._base_surfaces))) baseSurfObj = self._base_surfaces[basetype] baseVcoor = baseSurfObj['baseVerticesCoor'] baseFaceVertexIndices =baseSurfObj['baseFaceIndices'] if basetype == 'squ' : indexObj,vertexCoor = self._rectangulateBase(rez,baseVcoor,baseFaceVertexIndices, self._midVectorFun) else : indexObj,vertexCoor = self._init_triangulateBase(rez,baseVcoor,baseFaceVertexIndices, self._midVectorFun) faceIndices = indexObj['face'] super().__init__(vertexCoor, faceIndices, name, **kwargs) self.coorType = _COORSYS["XYZ"] self._normal_scale = _normLength( len(faceIndices), 0, 0, 4 ) self._rez = rez self._basetype = basetype return
[docs] def scale_dataframe(self,X,Y,Z) : """ Scaling the surface geometry based on a datagrid. Parameters ---------- X, Y, Z : N x M arrays Minimum and maximum values of the arrays are used to scale and translate the surface from an intial domain of [ (-1,1), (-1,1), (0,1) ] Returns ------- self : PlanarSurface object """ return _scale_dataframe(self,X,Y,Z)
[docs] def domain(self, xlim=None, ylim=None, zcoor=None) : """ Set the domain of the planar surface. Used for setting the limits of the base plane. Parameters ---------- xlim, ylim : 2d arrays Minimum and maximum values of the arrays are used to scale and translate the surface from an intial domains of [ -1, 1 ] If set to None, the domains are unchanged. zcoor: number, default : 0 Z offset of the plane. If set to None, the Z offset is unchanged. Returns ------- self : PlanarSurface object """ def setDomain(xyz, mx,bx,my,by,mz) : x,y,z = xyz X = mx*x + bx Y = my*y + by Z = z if mz is None else np.full(len(z),mz) return X,Y,Z def getCont( lims ) : m, b = 1.0, 0.0 if lims is not None : # allow single bndry to be input as a float if isinstance(lims, (int,float)) : lims = [-lims,lims] m, b = 0.5*(lims[1]-lims[0]) , 0.5*(lims[1]+lims[0]) return m, b mx, bx = getCont(xlim) my, by = getCont(ylim) self.map_geom_from_op(lambda A : setDomain(A, mx,bx,my,by,zcoor)) return self
[docs]class PolarSurface(Surface3DCollection) : """ Flat disk 3D surface in polar coordinates. Methods are inherited from the Surface3DCollection. Polar surface geometries are created in this subclass. """
[docs] @staticmethod def rand(rez=0,seed=None,name=None,kind=None,**kargs) : """ PolarSurface with random triangular faces. Parameters ---------- rez : number, optional, default: 0 Number of 'recursive' subdivisions of the triangulated base faces. Rez values range from 0 to 7. seed : integer, optional, default: None An initialization seed for random generation. name : string, optional, default: None. Descriptive identifier for the geometry. kind : string, optional, default: None (reserved, not implemented) Raises ------ ValueError If rez is not an integer in range 0 to 7. If basetype is not recognized for this surface constructor. Other Parameters ---------------- **kwargs All other parameters are passed on to 's3dlib.surface.Surface3DCollection'. Returns ------- surface : PolarSurface object. """ #......................................................... def random_linDist(size) : nLoop,nControl,samples = 0, 10**6, [] while len(samples)<size and nLoop<nControl: x=np.random.random_sample() prob=x/2 assert prob>=0 and prob<=1 if np.random.random_sample() <=prob: samples += [x] nLoop+=1 return np.array(samples) #......................................................... if not isinstance(rez, (int,float)) : raise ValueError('Incorrect rez type, must be a number.') if (rez<0) or (rez>_MAXREZ) : raise ValueError('Incorrest rez of {}, must be a number, 0 <= rez <= {}'.format(rez,_MAXREZ)) if seed is not None : if not isinstance(seed, int) : raise ValueError('Incorrect seed type, must ba of type int') np.random.seed(seed) N = int( 6*(4**rez)/2 + 3*(2**rez) + 1 ) r = random_linDist( N ) t = 2*np.pi*np.random.rand( N ) z = np.zeros( N ) data = PolarSurface.coor_convert([r,t,z],True).T rtdata = data[:,:2] tess = spatial.Delaunay(rtdata) if name is None : name = 'random mesh' surface = PolarSurface(name=name,**kargs) # set to the class default object # reset verts, faceIndices and edges (retain class coordinate system). verts = np.zeros( (N,3)) verts[:,:2] = tess.points faceIndices = tess.simplices surface.vertexCoor = verts surface.fvIndices = faceIndices surface.evIndices = surface._get_edges_from_faces(faceIndices) surface.vfIndicesList = surface._vertexCommonFaceIndices() surface.efIndicesList = surface._edgeCommonFaceIndices() surface.set_verts( verts[faceIndices] ) surface._set_geometric_bounds() surface._rez = str(rez) if isinstance(rez,int) else '{:.1f}'.format(rez) surface._basetype = 'rand_'+str(seed) return surface
@staticmethod def _get_polar_surfaces_dictionary() : """Constructs the dictionary of base polar surface geometries.""" surfacesDictionary = {} soff = 0.0001 x0 = 0.5 y0 = np.sqrt(3)/2 baseVert = [ [0,0,0], [1,0,0], [x0,y0,0], [-x0,y0,0], [-1,0,0], [-x0,-y0,0], [x0,-y0,0] ] qVert = [ [0,0,0], [1,0,0], [0,1,0], [-1,0,0], [0,-1,0] ] x0S, y0S = soff*y0, soff*x0 baseVertS = [ [1,-y0S,0], [1, y0S,0], [x0,y0,0], [-x0,y0,0], [-1,0,0], [-x0,-y0,0], [x0,-y0,0], [ x0S, y0S,0], [ 0, soff,0], [-x0S, y0S,0], [-x0S, -y0S,0], [ 0,-soff,0], [ x0S, -y0S,0] ] qVertS = [ [1,-soff,0], [1, soff,0], [0,1,0], [-1,0,0], [0,-1,0], [ soff, soff,0], [-soff ,soff,0], [-soff,-soff,0], [soff,-soff,0] ] baseFaces=([0,1,2],[0,2,3],[0,3,4],[0,4,5],[0,5,6],[0,6,1]) quadFaces=([0,1,2],[0,2,3],[0,3,4],[0,4,1]) baseFacesS=( [7,1,2], [8,2,3], [9,3,4], [10,4,5], [11,5,6], [12,6,0], [2,8,7], [3,9,8], [4,10,9], [5,11,10], [6,12,11] ) quadFacesS=( [5,1,2],[6,2,3],[7,3,4],[8,4,0], [2,6,5],[3,7,6],[4,8,7] ) Fs = len(quadFaces) Fh = len(baseFaces) FsS = len(quadFacesS) FhS = len(baseFacesS) square = { 'baseFaceIndices' : quadFaces , 'baseVerticesCoor' : qVert, 'fevParam' : [Fs,2,1] } hexag = { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fh,3,1] } squareS = { 'baseFaceIndices' : quadFacesS , 'baseVerticesCoor' : qVertS, 'fevParam' : [FsS,4.5,1] } hexagS = { 'baseFaceIndices' : baseFacesS , 'baseVerticesCoor' : baseVertS, 'fevParam' : [FhS,6.5,1] } surfacesDictionary['squ'] = square surfacesDictionary['hex'] = hexag surfacesDictionary['squ_s'] = squareS surfacesDictionary['hex_s'] = hexagS # ... 'special case' only for fev method for polar surfaces. surfacesDictionary['squ_c'] = { 'fevParam' : [16,5,1] } surfacesDictionary['hex_c'] = { 'fevParam' : [12,4,1] } return surfacesDictionary @staticmethod def _midVectorFun(vectA, vectB) : """Mid-point at the mid radial position between two points in a plane.""" mid = np.add(vectA,vectB) mid = np.multiply(0.5,mid) a = (np.linalg.norm(vectA)+np.linalg.norm(vectB))/2 b = np.linalg.norm(mid) mid = np.multiply(mid,a/b) return mid @staticmethod def _map3Dto2Dsquare(xyz) : """Override map surface to rectagular unit coordinates (0,1).""" x,y,_ = xyz theta = np.arctan2(y,x) theta = np.where( theta<0, 2.0*np.pi + theta ,theta) a = theta/(2*np.pi) r = np.sqrt(x*x+y*y) b = 1-r a = np.clip(a,0.0,1.0) b = np.clip(b,0.0,1.0) return [a,b]
[docs] @staticmethod def coor_convert(xyz, tocart=False) : """ Tranformation between polar and Cartesian coordinates. The angular coordinate is in radians with domain 0 to 2pi. Parameters ---------- xyz : 3xN array N number of vectors in either polar or cartesian coordinates. tocart : { True, False }, default: False If True, input is polar and output is cartesian. If False, input is cartesian and output is polar. Returns ------- list: 3xN array Transformed coordinates. """ if tocart : r, theta, z = xyz x = r * np.cos(theta) y = r * np.sin(theta) abc = [x,y,z] else: x,y,z = xyz R = np.sqrt( x*x + y*y ) theta = np.arctan2(y,x) theta = np.where(theta<0,theta+2*np.pi,theta) abc = [R,theta,z] abc = np.array(abc) return abc
_base_surfaces = _get_polar_surfaces_dictionary.__func__() _default_base = 'hex' # default assignment is indicated in __init Docstring. _geomType = _COORSYS['POLAR'] @classmethod def _uvw_to_xyz( cls,uvw, rtz) : """Override rotational transformation at a polar coordinate.""" return super()._disp_to_xyz( uvw, rtz[1] )
[docs] @classmethod def grid(cls,nrad, nang, basetype='d', minrad=None, name=None, **kwargs) : """ PolarSurface with axial and radial faces. Parameters ---------- nrad : integer, required. Number of radial subdivisions. Minimum value is 1 nang : integer, required. Number of angular subdivisions. Minimum value is 3. basetype : {'d','s','q','w','r','x'}, optional, default: 'd' Starting surface geometries from which the surface is constructed indicating face shape and if split. minrad : scalar, optional, default: 0.01 The minimum distance from the z-axis of any vertex coordinate. Use is dependent on basetype. name : string, optional, default: None. Descriptive identifier. Raises ------ ValueError If nrad is not an integer greater or equal to 1. If nang is not an integer greater or equal to 3. Other Parameters ---------------- **kwargs All other parameters are passed on to 's3dlib.surface.Surface3DCollection'. Returns ------- surface : PolarSurface object. """ Nfaces = { 'd': 3, 's': 3, 'q':6, 'w':6, 'r':3, 'x':3 } # 1,3 splitSize = None surface = cls._square_net(nrad, nang, basetype,minrad, splitSize, name, **kwargs) surface._postProc_surfaceColors(True) surface._rez = surface._calc_rez( 6 ) return surface
[docs] @classmethod def pntsurf(cls, rtz_points, name=None, **kargs) : """ PolarSurface from points in polar coordinates. Parameters ---------- rtz_points : array, required. An N X 3, or N X 4, array of N polar coordinate points. For an N X 4 array, the 4th is the vertex scalar value. name : string, optional, default: None. Descriptive identifier for the point surface. Returns ------- surface : PolarSurface object """ dataPts = np.array(rtz_points) # update for v_1.3.0 dataPts, vVal = Surface3DCollection._extract_vals_from_inputCoor(dataPts) xyzPts = PolarSurface.coor_convert(dataPts.T,True) fvIndices = tri.Triangulation(xyzPts[0],xyzPts[1]).triangles base_surface = Surface3DCollection(dataPts, fvIndices, color='tan').shade() faceIndices = base_surface.fvIndices verts = xyzPts.T if name is None : name = 'point surface' surface = PolarSurface(name=name,**kargs) surface.vertexCoor = verts surface.fvIndices = faceIndices surface.evIndices = surface._get_edges_from_faces(faceIndices) surface.vfIndicesList = surface._vertexCommonFaceIndices() surface.efIndicesList = surface._edgeCommonFaceIndices() surface.set_verts( verts[faceIndices] ) surface._set_geometric_bounds() surface._rez = surface._calc_rez(4) if vVal is not None : surface.set_vertvals(vVal) # update for v_1.3.0 surface._basetype = 'pntsurf' return surface
@classmethod def _Xfev(cls,rez, basetype=None, minrad=None) : """ Calculates number of faces, edges, and vertices of a PolarSurface. Parameters ---------- rez : integer Number of recursive subdivisions of a triangulated base faces. basetype : string Starting surface geometries from which the surface would be constructed using recursive subdivisions of the triangular faces. Returns ------- list: the number of faces, edges, and vertices. """ if isinstance(rez,(list,tuple)) : # check for disk... nrad = rez[0] nang = rez[1] # only check is ending in s isSplit = basetype[ len(basetype)-1] == 's' if minrad is None : f = 2*nrad*nang - nang e = 3*nrad*nang - nang v = nrad*nang + 1 if isSplit : e += nrad v += nrad else : # any value will trigger this condition. f = 2*nrad*nang e = 3*nrad*nang + nang v = (nrad+1)*nang if isSplit : e += nrad v += nrad + 1 return [f,e,v] return super()._fev(rez,basetype,cls) @staticmethod def _flat_split_cyclinder(rez,basetype,minrad, **kwargs) : """ Transform cylindrical base surface to polar coordinates. note : This method provides an alternative to the poler basetypes of 'hex_s' and 'squ_s'. The grids constructed here may provide smoother geometries depending on the mapping functions. """ # ................................................. def cyl2pol(rtz): r,t,z = rtz b = (1.0+minrad)/2.0 m = -(1.0-minrad)/2.0 R = m*z + b Z = 0*z return R,t,Z # ................................................. if basetype == 'squ_c' : cylbase = 'squ_s' if basetype == 'hex_c' : cylbase = 'tri_s' cyl_obj = CylindricalSurface(rez,cylbase, **kwargs).map_geom_from_op(cyl2pol) baseFaceVertexIndices = cyl_obj._base_surfaces[cylbase] ['baseFaceIndices'] fvIndices = cyl_obj.fvIndices vertexCoor = cyl_obj.vertexCoor evIndices = cyl_obj.evIndices return vertexCoor, fvIndices, evIndices, baseFaceVertexIndices, def __init__(self, rez=0, basetype=None, minrad=None, name=None, **kwargs): """ Create flat disk surface of unit radius. Parameters ---------- rez : integer, optional, default: 0 Number of recursive subdivisions of the triangulated base faces. Rez values range from 0 to 7. basetype : {'squ','hex','squ_s','hex_s','squ_c','hex_c'}, optional, default: 'hex' Starting surface geometries from which the surface is constructed using recursive subdivisions of the triangular faces. minrad : scalar, optional, default: 0.01 For basetypes 'squ_c' and 'hex_c, the minimum distance from the z-axis of any vertex coordinate. name : string, optional, default: None. Descriptive identifier for the geometry. Raises ------ ValueError If rez is not an integer in range 0 to 7. If basetype is not recognized for this surface constructor. Other Parameters ---------------- **kwargs All other parameters are passed on to 's3dlib.surface.Surface3DCollection'. """ if minrad is None : minrad = _MINRAD if not isinstance(rez, int) : raise ValueError('Incorrect rez type, must ba of type int') if (rez<0) or (rez>_MAXREZ) : raise ValueError('Incorrest rez of {}, must be an int, 0 <= rez <= {}'.format(rez,_MAXREZ)) if basetype is None : basetype = self._default_base # ... check for 'special case' (ugly code but needed) if basetype == 'squ_c' or basetype == 'hex_c' : vertexCoor, faceIndices, edgeIndices, bfvi = self._flat_split_cyclinder(rez,basetype,minrad, **kwargs) baseFaceVertexIndices = bfvi else : if basetype not in self._base_surfaces : raise ValueError('Basetype {} is not recognized. Possible values are: {}'.format(basetype,list(self._base_surfaces))) baseSurfObj = self._base_surfaces[basetype] baseVcoor = baseSurfObj['baseVerticesCoor'] baseFaceVertexIndices =baseSurfObj['baseFaceIndices'] indexObj,vertexCoor = self._init_triangulateBase(rez,baseVcoor,baseFaceVertexIndices, self._midVectorFun) faceIndices = indexObj['face'] edgeIndices = indexObj['edge'] super().__init__(vertexCoor, faceIndices, name, **kwargs) self.coorType = _COORSYS["POLAR"] self._normal_scale = _normLength( len(faceIndices), 1.2, 1.55, np.pi ) self._rez = rez self._basetype = basetype return
[docs] def domain(self, radius=None, zcoor=None) : """ Set the domain of the polar surface. Used for setting the limits of the base polar plane. Parameters ---------- radius: number, default : 1 Radius of the polar surface. If set to None, the radius is unchanged. zcoor: number, default : 0 Z offset of the polar plane. If set to None, the Z offset is unchanged. Returns ------- self : PolarSurface object """ def setDomain(rtz, mr, mz) : r,t,z = rtz R = mr*r Z = z if mz is None else np.full(len(z),mz) return R,t,Z def getCont(lims) : m = 1.0 if lims is not None : m = lims return m mr = getCont(radius) self.map_geom_from_op(lambda A : setDomain(A, mr,zcoor )) return self
[docs]class CylindricalSurface(Surface3DCollection) : """ Cylindrical 3D surface in cylindrical coordinates. Methods are inherited from the Surface3DCollection. Cylindrical surface geometries are created in this subclass. """
[docs] @staticmethod def rand(rez=0,seed=None,name=None,kind=None,**kargs) : """ CylindricalSurface with random triangular faces. Parameters ---------- rez : number, optional, default: 0 Number of 'recursive' subdivisions of the triangulated base faces. Rez values range from 0 to 7. seed : integer, optional, default: None An initialization seed for random generation. name : string, optional, default: None. Descriptive identifier for the geometry. kind : string, optional, default: None (reserved, not implemented) Raises ------ ValueError If rez is not an integer in range 0 to 7. If basetype is not recognized for this surface constructor. Other Parameters ---------------- **kwargs All other parameters are passed on to 's3dlib.surface.Surface3DCollection'. Returns ------- surface : CylindricalSurface object. """ #......................................................... def random_sinDist(size) : nLoop,nControl,samples = 0, 10**6, [] while len(samples)<size and nLoop<nControl: x=np.random.random_sample() prob=np.sin(x*np.pi)/2 assert prob>=0 and prob<=1 if np.random.random_sample() <=prob: samples += [x] nLoop+=1 return np.array(samples) #......................................................... if not isinstance(rez, (int, float)) : raise ValueError('Incorrect rez type, must be a number.') if (rez<0) or (rez>_MAXREZ) : raise ValueError('Incorrest rez of {}, must be a number, 0 <= rez <= {}'.format(rez,_MAXREZ)) if seed is not None : if not isinstance(seed, int) : raise ValueError('Incorrect seed type, must ba of type int') np.random.seed(seed) N = int(12*(4**rez)/2 + 3*(2**rez)) r = np.ones( N ) t = np.random.uniform(0,2*np.pi, N) z = np.random.uniform(-1,1, N) dataPts = np.array( [r,t,z] ).T if name is None : name = 'random mesh' surface = CylindricalSurface.pntsurf(dataPts, name=None, **kargs) surface._rez = str(rez) if isinstance(rez,int) else '{:.1f}'.format(rez) surface._basetype = 'rand_'+str(seed) return surface
@staticmethod def _get_cylindrical_surfaces_dictionary() : """Constructs the dictionary of base cylindrical surface geometries. """ surfacesDictionary = {} def construct_tricylinder() : y0 = np.sqrt(3.0)/2.0 x0 = 0.5 baseVert = [ [1,0,0], [-x0,-y0,0], [-x0,y0,0], [-1,0,1], [x0,-y0,1], [x0,y0,1], [-1,0,-1], [x0,-y0,-1], [x0,y0,-1] ] baseFaces=( [0,2,5],[2,1,3],[1,0,4], [0,5,4],[2,3,5],[1,4,3], [0,8,2],[2,6,1],[1,7,0], [0,7,8],[2,8,6],[1,6,7]) Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,3,0] } def construct_tricylinder_v() : vfi_d = construct_tricylinder() baseVert = vfi_d['baseVerticesCoor'] baseFaces = vfi_d['baseFaceIndices'] baseVert = baseVert + [[0,0,1],[0,0,-1]] baseFaces = list(baseFaces) addFaces = [ [3,4,9],[4,5,9],[5,3,9], [6,8,10],[8,7,10],[7,6,10] ] baseFaces = baseFaces + addFaces Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,0,2] } def construct_tri2cylinder() : y0 = np.sqrt(3.0)/2.0 x0 = 0.5 baseVert = [ [-1,0,0], [x0,-y0,0], [x0,y0,0], [1,0,1], [-x0,-y0,1], [-x0,y0,1], [1,0,-1], [-x0,-y0,-1], [-x0,y0,-1] ] baseFaces=( [0,4,5],[1,3,4],[2,5,3], [0,8,7],[1,7,6],[2,6,8], [0,7,4],[1,6,3],[2,8,5], [0,5,8],[1,4,7],[2,3,6]) Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,3,0] } def construct_tri2cylinder_v() : vfi_d = construct_tri2cylinder() baseVert = vfi_d['baseVerticesCoor'] baseFaces = vfi_d['baseFaceIndices'] baseVert = baseVert + [[0,0,1],[0,0,-1]] baseFaces = list(baseFaces) addFaces = [ [3,9,4],[4,9,5],[5,9,3], [6,10,8],[7,10,6],[8,10,7] ] baseFaces = baseFaces + addFaces Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,0,2] } def construct_tri2cylinder_sliced() : y0 = np.sqrt(3.0)/2.0 x0 = 0.5 yof = 0.0001 baseVert = [ [-1,0,0], [x0,-y0,0], [x0,y0,0], [1,yof,1], [-x0,-y0,1], [-x0,y0,1], [1,yof,-1], [-x0,-y0,-1], [-x0,y0,-1], [1,-yof,1], [1,-yof,-1] ] baseFaces=( [0,4,5],[1,9,4],[2,5,3], [0,8,7],[1,7,10],[2,6,8], [0,7,4],[1,10,9],[2,8,5], [0,5,8],[1,4,7],[2,3,6]) Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,4,1] } def construct_tri2cylinder_sliced_v() : vfi_d = construct_tri2cylinder_sliced() baseVert = vfi_d['baseVerticesCoor'] baseFaces = vfi_d['baseFaceIndices'] baseVert = baseVert + [[0,0,1],[0,0,-1]] baseFaces = list(baseFaces) addFaces = [ [3,5,11],[5,4,11],[4,9,11], [8,6,12],[7,8,12],[10,7,12] ] baseFaces = baseFaces + addFaces Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,0,2] } def construct_quadcylinder() : xy0 = 1.0/np.sqrt(2.0) baseVert = [ [1,0,0], [0,-1,0], [-1,0,0], [0,1,0], [xy0,xy0,1], [-xy0,xy0,1], [-xy0,-xy0,1], [xy0,-xy0,1], [xy0,xy0,-1], [-xy0,xy0,-1], [-xy0,-xy0,-1], [xy0,-xy0,-1] ] baseFaces=( [1,0,7],[2,1,6],[3,2,5],[0,3,4], [4,3,5],[5,2,6],[6,1,7],[7,0,4], [0,8,3],[3,9,2],[2,10,1],[1,11,0], [0,11,8],[3,8,9],[2,9,10],[1,10,11] ) Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,4,0] } def construct_quadcylinder_v() : vfi_d = construct_quadcylinder() baseVert = vfi_d['baseVerticesCoor'] baseFaces = vfi_d['baseFaceIndices'] baseVert = baseVert + [[0,0,1],[0,0,-1]] baseFaces = list(baseFaces) addFaces = [ [4,5,12],[5,6,12],[6,7,12],[7,4,12], [9,8,13],[10,9,13],[11,10,13],[8,11,13] ] baseFaces = baseFaces + addFaces Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,0,2] } def construct_quad2cylinder() : xy0 = 1.0/np.sqrt(2.0) baseVert = [ [xy0,xy0,0], [-xy0,xy0,0], [-xy0,-xy0,0], [xy0,-xy0,0], [1,0,1], [0,-1,1], [-1,0,1], [0,1,1], [1,0,-1], [0,-1,-1], [-1,0,-1], [0,1,-1] ] baseFaces=( [0,7,4],[1,6,7],[2,5,6],[3,4,5], [0,8,11],[1,11,10],[2,10,9],[3,9,8], [0,4,8],[1,7,11],[2,6,10],[3,5,9], [0,11,7],[1,10,6],[2,9,5],[3,8,4] ) Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,4,0] } def construct_quad2cylinder_v() : vfi_d = construct_quad2cylinder() baseVert = vfi_d['baseVerticesCoor'] baseFaces = vfi_d['baseFaceIndices'] baseVert = baseVert + [[0,0,1],[0,0,-1]] baseFaces = list(baseFaces) addFaces = [ [5,4,12],[6,5,12],[7,6,12],[4,7,12], [8,9,13],[9,10,13],[10,11,13],[11,8,13] ] baseFaces = baseFaces + addFaces Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,0,2] } def construct_quad2cylinder_sliced() : xy0 = 1.0/np.sqrt(2.0) yof = 0.0001 baseVert = [ [xy0,xy0,0], [-xy0,xy0,0], [-xy0,-xy0,0], [xy0,-xy0,0], [1,yof,1], [0,-1,1], [-1,0,1], [0,1,1], [1,yof,-1], [0,-1,-1], [-1,0,-1], [0,1,-1], [1,-yof,1], [1,-yof,-1] ] baseFaces=( [0,7,4],[1,6,7],[2,5,6],[3,12,5], [0,8,11],[1,11,10],[2,10,9],[3,9,13], [0,4,8],[1,7,11],[2,6,10],[3,5,9], [0,11,7],[1,10,6],[2,9,5],[3,13,12] ) Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,5,1] } def construct_quad2cylinder_sliced_v() : vfi_d = construct_quad2cylinder_sliced() baseVert = vfi_d['baseVerticesCoor'] baseFaces = vfi_d['baseFaceIndices'] baseVert = baseVert + [[0,0,1],[0,0,-1]] baseFaces = list(baseFaces) addFaces = [ [4,7,14],[7,6,14],[6,5,14],[5,12,14], [11,8,15],[10,11,15],[9,10,15],[13,9,15] ] baseFaces = baseFaces + addFaces Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,0,2] } surfacesDictionary['tri'] = construct_tricylinder() surfacesDictionary['squ'] = construct_quadcylinder() surfacesDictionary['tri2'] = construct_tri2cylinder() surfacesDictionary['squ2'] = construct_quad2cylinder() surfacesDictionary['tri_s'] = construct_tri2cylinder_sliced() surfacesDictionary['squ_s'] = construct_quad2cylinder_sliced() surfacesDictionary['tri_v'] = construct_tricylinder_v() surfacesDictionary['squ_v'] = construct_quadcylinder_v() surfacesDictionary['tri2_v'] = construct_tri2cylinder_v() surfacesDictionary['squ2_v'] = construct_quad2cylinder_v() surfacesDictionary['tri_sv'] = construct_tri2cylinder_sliced_v() surfacesDictionary['squ_sv'] = construct_quad2cylinder_sliced_v() return surfacesDictionary @staticmethod def _midVectorFun(vectA, vectB) : """Mid-point between two points on a unit cylinder.""" mid = np.add(vectA,vectB) mid = np.multiply(0.5,mid) z = np.dot(mid,[0,0,1]) v = np.subtract(mid,[0,0,z]) xy = v/np.linalg.norm(v) # xy is unit vector at cylinder radius, but # is average of radius for top & bottom faces. zA = np.dot(vectA,[0,0,1]) vA = np.subtract(vectA,[0,0,zA]) zB = np.dot(vectB,[0,0,1]) vB = np.subtract(vectB,[0,0,zB]) a = ( np.linalg.norm(vA) + np.linalg.norm(vB) )/2.0 xy = a*xy return np.add(xy,[0,0,z]) @staticmethod def _map3Dto2Dsquare(xyz) : """Override map surface to rectagular unit coordinates (0,1).""" x,y,z = xyz theta = np.arctan2(y,x) theta = np.where( theta<0, 2.0*np.pi + theta ,theta) a = theta/(2*np.pi) b = z/2 + 0.5 a = np.clip(a,0.0,1.0) b = np.clip(b,0.0,1.0) return [a,b]
[docs] @staticmethod def coor_convert(xyz, tocart=False) : """ Transformation between cylindrical and Cartesian coordinates. The angular coordinate is in radians with domain 0 to 2pi. Parameters ---------- xyz : 3xN N number of vectors in either cylindrical or cartesian coordinates, including the z coordinate( which remains unchanged). tocart : { True, False }, default: False If True, input is cylindrical and output is cartesian. If False, input is cartesian and output is cylindrical. Returns ------- list: 3xN array Transformed coordinates. """ if tocart : r, theta, z = xyz x = r * np.cos(theta) y = r * np.sin(theta) abc = [x,y,z] else: x,y,z = xyz R = np.sqrt( x*x + y*y ) theta = np.arctan2(y,x) theta = np.where(theta<0,theta+2*np.pi,theta) abc = [R,theta,z] abc = np.array(abc) return abc
_base_surfaces = _get_cylindrical_surfaces_dictionary.__func__() _default_base = 'tri' # default assignment is indicated in __init Docstring. _geomType = _COORSYS['CYLINDRICAL'] @classmethod def _cyl_vol_square_net(cls, numZ, numU, basetype,minRad, splitSize, numR, name=None, **kwargs) : """ method only called by the CylinderSurface classmethod grid ONLY for volume cylinder objects. """ # DevNote: this method uses the same algorithm as the base class _square_net classmethod. # This was done to simplify the code for this special case rather than # add additional 'convoluted' logic to the _square_net method. # For this case, the algorithm combines one cylindrical and two polar grids. # Note: numX and num_X are the number of divisions and the number of coor positions. if numR is None : numR = int((numZ+1)/2) if basetype is None : basetype = 'd' if minRad is None : minRad = _MINRAD if splitSize is None : splitSize = _SPLITSIZE basetype = basetype.lower() baselist = ['d','s','q','w','r','x'] if basetype not in baselist : raise ValueError("Grid basetype '{}' is not recognized. Possible values are: {}.".format(basetype,list(baselist))) isSplit = (basetype == 's') or (basetype == 'w') or (basetype == 'x') needsTriag = not ( (basetype == 'r') or (basetype == 'x') ) hasCenter = (basetype == 'd') or (basetype == 's') # T angle ranges ..... num_U = numU+1 if isSplit else numU min_U = 0.0 max_U = (1-1/num_U)*2*np.pi if isSplit : max_U = (2-splitSize)*np.pi coor_U = np.linspace(min_U,max_U,num_U) # R disk ranges ........ if hasCenter : num_R,inn_R = numR-1, 1.0/numR out_R = 1.0-(1.0/numR) else : num_R,inn_R = numR, minRad out_R = 1.0-(1.0/numR) + minRad/numR coor_R_bot = np.linspace(inn_R,out_R, num_R) coor_R_mid = np.full(numZ+1,1.0) coor_R_top = np.linspace(out_R,inn_R, num_R) num_Z = numZ+1 coor_Z_bot = np.full(num_R,-1.0) coor_Z_mid = np.linspace(-1.0,1.0,num_Z) coor_Z_top = np.full(num_R, 1.0) coor_R = np.concatenate( (coor_R_bot,coor_R_mid,coor_R_top),axis=0) coor_Z = np.concatenate( (coor_Z_bot,coor_Z_mid,coor_Z_top),axis=0) numV = 2*numR + numZ num_V = 2*num_R + num_Z totVerts = num_U*num_V U_coor = np.tile(coor_U,num_V) R_coor = np.tile(coor_R,(num_U,1)) R_coor = np.ravel(R_coor,order='F') Z_coor = np.tile(coor_Z,(num_U,1)) Z_coor = np.ravel(Z_coor,order='F') abc = np.array([R_coor,U_coor,Z_coor]) xyz = CylindricalSurface.coor_convert(abc,True) verts = np.array(xyz).T numVt = num_V-1 if hasCenter else numV faceIndices = cls._grid_face_indices(numU,numVt,isSplit,needsTriag) if hasCenter : # add top triangular faces ... topVert = np.array([[0.0,0.0,1]]) topIndex = totVerts startIndex = topIndex - num_U endIndex = topIndex-2 if isSplit else topIndex-1 a = np.linspace(startIndex,endIndex,numU,dtype=int) b = a+1 if isSplit else np.append( a[1:], [startIndex] ) c = np.full(numU,topIndex) abc = np.array([a,b,c]).T verts = np.append(verts,topVert, axis=0) faceIndices = np.append(faceIndices,abc, axis=0) # add bottom triangular facess ... btmVert = np.array([[0.0,0.0,-1]]) btmIndex = totVerts+1 startIndex,endIndex = 0,numU-1 a = np.linspace(startIndex,endIndex,numU,dtype=int) b = a+1 if isSplit else np.append( a[1:], [startIndex] ) c = np.full(numU,btmIndex) bac = np.array([b,a,c]).T # bac instead for abc for outward normals. verts = np.append(verts,btmVert, axis=0) faceIndices = np.append(faceIndices,bac, axis=0) surface = cls(**kwargs) # set to the class default object # reset verts, faceIndices and edges (retain class coordinate system). surface.vertexCoor = verts surface.fvIndices = faceIndices surface.evIndices = surface._get_edges_from_faces(faceIndices) surface.vfIndicesList = surface._vertexCommonFaceIndices() surface.efIndicesList = surface._edgeCommonFaceIndices() surface.set_verts( verts[faceIndices] ) surface._set_geometric_bounds() surface.name = name surface._basetype = 'grid_'+basetype+'v' return surface @classmethod def _uvw_to_xyz(cls, uvw, rtz) : """Override rotational tranformation at a cylindrical coordinate.""" return super()._disp_to_xyz( uvw, rtz[1], None )
[docs] @classmethod def grid(cls, nver ,nang, basetype='d',minrad=None, numR=None, name=None, **kwargs) : """ CylindricalSurface with axial and vertical faces. Parameters ---------- nver : integer, required. Number of vertical subdivisions. Minimum value is 1 nang : integer, required. Number of angular subdivisions. Minimum value is 3. basetype : {'d','q','r','s','w','x', 'dv','qv','rv','sv','wv','xv'}, optional, default: 'd' Starting surface geometries from which the surface is constructed indicating face shape and if split. minrad : scalar, optional, default: 0.01 The minimum distance from the z-axis of any vertex coordinate. Use is dependent on basetype. numR : integer, optional, default: None Only applicable for volume cylinder geometries. Number of radial subdivisions for volume cylinders. When None, default is half the number of vertical subdivisions. name : string, optional, default: None. Descriptive identifier. Raises ------ ValueError If nver is not an integer greater or equal to 1. If nang is not an integer greater or equal to 3. Other Parameters ---------------- **kwargs All other parameters are passed on to 's3dlib.surface.Surface3DCollection'. Returns ------- surface : CylindricalSurface object. """ Nfaces = { 'd': 6, 's': 6, 'q':6, 'w':6, 'r':3, 'x':3, 'dv': 12, 'sv': 12, 'qv':18, 'wv':18, 'rv':9, 'xv':9 } # 1,3 splitSize = None # FutDev: currently, not user accessable. dftNfaces = 12 basetype = basetype.lower() if basetype.endswith('v') : dftNfaces = 18 subtype = basetype[0] surface = CylindricalSurface._cyl_vol_square_net( nver ,nang, subtype, minrad, splitSize, numR, name, **kwargs) else : surface = cls._square_net(nver ,nang,basetype,minrad,splitSize, name, **kwargs) surface._postProc_surfaceColors(True) surface._rez = surface._calc_rez( dftNfaces ) return surface
[docs] @classmethod def pntsurf(cls, rtz_points, name=None, **kargs) : """ CylindricalSurface from points in cylindrical coordinates. Parameters ---------- rtz_points : array, required. An N X 3, or N X 4, array of N cylindrical coordinate points. For an N X 4 array, the 4th is the vertex scalar value. name : string, optional, default: None. Descriptive identifier for the point surface. Returns ------- surface : CylindricalSurface object """ dataPts = np.array(rtz_points) # update for v_1.3.0 dataPts, vVal = Surface3DCollection._extract_vals_from_inputCoor(dataPts) cylPts = np.array(dataPts.T) cylPts[0] = np.amax(cylPts[0]) xyz_cylPts = CylindricalSurface.coor_convert(cylPts,True) minZ,maxZ = np.amin(cylPts[2]), np.amax(cylPts[2]) offsetZ = (maxZ+minZ)/2 xyz_cylPts[2] = xyz_cylPts[2] - offsetZ sphPts = SphericalSurface.coor_convert(xyz_cylPts,False) rmax = np.amax(sphPts[0]) sphPts[0] = rmax xyz_sphPts = SphericalSurface.coor_convert(sphPts,True) ptTop,ptBtm = [0,0,rmax], [0,0,-rmax] xyz_sphPts = np.append(xyz_sphPts.T,[ptTop,ptBtm],axis=0).T # note: the following surface method chull corrects face normal # orientations so spatial.ConvexHull was not directly used. hull_surface = Surface3DCollection.chull(xyz_sphPts.T) verts = CylindricalSurface.coor_convert(dataPts.T,True).T # remove faces attached to the two top & bottom vertices. N = len(dataPts) fvIndices = hull_surface.fvIndices maxIndices = np.max(fvIndices,axis=1) shouldkeep = np.where(maxIndices < N) faceIndices = fvIndices[shouldkeep] if name is None : name = 'point surface' surface = CylindricalSurface(name=name,**kargs) surface.vertexCoor = verts surface.fvIndices = faceIndices surface.evIndices = surface._get_edges_from_faces(faceIndices) surface.vfIndicesList = surface._vertexCommonFaceIndices() surface.efIndicesList = surface._edgeCommonFaceIndices() surface.set_verts( verts[faceIndices] ) surface._set_geometric_bounds() surface._rez = surface._calc_rez(12) surface._basetype = 'pntsurf' if vVal is not None : surface.set_vertvals(vVal) # update for v_1.3.0 return surface
@classmethod def _Xfev(cls,rez, basetype=None, minrad=None) : """ Calculates number of faces, edges, and vertices of a CylindricalSurface. Parameters ---------- rez : integer Number of recursive subdivisions of a triangulated base faces. basetype : string Starting surface geometries from which the surface would be constructed using recursive subdivisions of the triangular faces. Returns ------- list: the number of faces, edges, and vertices. """ if isinstance(rez,(list,tuple)) : # check for ring... nver = rez[0] nang = rez[1] # only check is ending in s isSplit = basetype[ len(basetype)-1] == 's' f = 2*nver*nang e = 3*nver*nang + nang v = (nver + 1)*nang if isSplit : e += nver v += nver + 1 return [f,e,v] return super()._fev(rez,basetype,cls) def __init__(self, rez=0, basetype=None, name=None, **kwargs): """ Create cylindrical surface of unit radius and length 2. Parameters ---------- rez : integer, optional, default: 0 Number of recursive subdivisions of the triangulated base faces. Rez values range from 0 to 7. basetype : {'tri','squ','tri2','squ2','tri_s','squ_s', 'tri_v','squ_v','tri2_v','squ2_v','tri_sv','squ_sv'}, optional, default: 'tri' Starting surface geometries from which the surface is constructed using recursive subdivisions of the triangular faces. name : string, optional, default: None. Descriptive identifier for the geometry. Raises ------ ValueError If rez is not an integer in range 0 to 7. If basetype is not recognized for this surface constructor. Other Parameters ---------------- **kwargs All other parameters are passed on to 's3dlib.surface.Surface3DCollection'. """ if not isinstance(rez, int) : raise ValueError('Incorrect rez type, must ba of type int') if (rez<0) or (rez>_MAXREZ) : raise ValueError('Incorrest rez of {}, must be an int, 0 <= rez <= {}'.format(rez,_MAXREZ)) if basetype is None : basetype = self._default_base if basetype not in self._base_surfaces : raise ValueError('Basetype {} is not recognized. Possible values are: {}'.format(basetype,list(self._base_surfaces))) baseSurfObj = self._base_surfaces[basetype] baseVcoor = baseSurfObj['baseVerticesCoor'] baseFaceVertexIndices =baseSurfObj['baseFaceIndices'] indexObj,vertexCoor = self._init_triangulateBase(rez,baseVcoor,baseFaceVertexIndices, self._midVectorFun) faceIndices = indexObj['face'] super().__init__(vertexCoor, faceIndices, name, **kwargs) self.coorType = _COORSYS["CYLINDRICAL"] self._normal_scale = _normLength( len(faceIndices), 1.14, 1.31) self._rez = rez self._basetype = basetype self.disp_Vector = np.array( [1,1,0] ) return None
[docs] def domain(self, radius=None, zlim=None) : """ Set the domain of the cylindrical surface. Used for setting the limits of the base cylinder. Parameters ---------- radius: number, default : 1 Radius of the cylindrical surface. If set to None, the domain is unchanged. zlim : 2d array Minimum and maximum values of the arrays are used to scale and translate the surface from an intial domain of [ -1, 1 ] If set to None, the Z-domain is unchanged. Returns ------- self : CylindricalSurface object """ def setDomain(rtz, mr, mz,bz) : r,t,z = rtz R = mr*r Z = mz*z + bz return R,t,Z def getCont_r(lims) : m = 1.0 if lims is not None : m = lims return m def getCont( lims ) : m, b = 1.0, 0.0 if lims is not None : # allow single bndry to be input as a float if isinstance(lims, (int,float)) : lims = [-lims,lims] m, b = 0.5*(lims[1]-lims[0]) , 0.5*(lims[1]+lims[0]) return m, b mr = getCont_r(radius) mz, bz = getCont(zlim) self.map_geom_from_op(lambda A : setDomain(A, mr, mz,bz)) return self
[docs]class SphericalSurface(Surface3DCollection) : """ Spherical 3D surface in spherical coordinates. Methods are inherited from the Surface3DCollection. Spherical surface geometries are created in this subclass. """
[docs] @staticmethod def rand(rez=0,seed=None,name=None,kind=None,**kargs) : """ SphericalSurface with random triangular faces. Parameters ---------- rez : number, optional, default: 0 Number of 'recursive' subdivisions of the triangulated base faces. Rez values range from 0 to 7. seed : integer, optional, default: None An initialization seed for random generation. name : string, optional, default: None. Descriptive identifier for the geometry. kind : string, optional, default: None (reserved, not implemented) Raises ------ ValueError If rez is not an integer in range 0 to 7. If basetype is not recognized for this surface constructor. Other Parameters ---------------- **kwargs All other parameters are passed on to 's3dlib.surface.Surface3DCollection'. Returns ------- surface : SphericalSurface object. """ #......................................................... def random_sinDist(size) : nLoop,nControl,samples = 0, 10**6, [] while len(samples)<size and nLoop<nControl: x=np.random.random_sample() prob=np.sin(x*np.pi)/2 assert prob>=0 and prob<=1 if np.random.random_sample() <=prob: samples += [x] nLoop+=1 return np.array(samples) #......................................................... if not isinstance(rez, (int, float)) : raise ValueError('Incorrect rez type, must be a number.') if (rez<0) or (rez>_MAXREZ) : raise ValueError('Incorrest rez of {}, must be a number, 0 <= rez <= {}'.format(rez,_MAXREZ)) if seed is not None : if not isinstance(seed, int) : raise ValueError('Incorrect seed type, must ba of type int') np.random.seed(seed) N = int(20*(4**rez)/2 + 2) r = np.ones( N ) t = 2*np.pi*np.random.rand( N ) p = np.pi*random_sinDist( N ) data = SphericalSurface.coor_convert([r,t,p],True).T hullsurf = Surface3DCollection.chull(data) if name is None : name = 'random mesh' surface = SphericalSurface(name=name,**kargs) # set to the class default object # reset verts, faceIndices and edges (retain class coordinate system). verts = hullsurf.vertexCoor faceIndices = hullsurf.fvIndices surface.vertexCoor = verts surface.fvIndices = faceIndices surface.evIndices = surface._get_edges_from_faces(faceIndices) surface.vfIndicesList = surface._vertexCommonFaceIndices() surface.efIndicesList = surface._edgeCommonFaceIndices() surface.set_verts( verts[faceIndices] ) surface._set_geometric_bounds() surface._rez = str(rez) if isinstance(rez,int) else '{:.1f}'.format(rez) surface._basetype = 'rand_'+str(seed) return surface
[docs] @staticmethod def platonic(rez=0,basetype=None,name=None, **kwargs) : """ Platonic Solid surfaces. Parameters ---------- rez : integer, optional, default: 0 Number of recursive subdivisions of the triangulated base faces. Rez values range from 0 to 7. basetype : {'tetra','octa','icosa','cube','dodeca'}, optional, default: 'icosa' Starting surface geometries from which the surface is constructed using recursive subdivisions of the triangular faces. name : string, optional, default: None. Descriptive identifier for the geometry. Returns ------- SphericalSurface object Other Parameters ---------------- **kwargs All other parameters are passed on to 's3dlib.surface.SphericalSurface'. """ #..................................... def fev_cube_a() : v = [ [-1,-1,-1],[-1, 1,-1],[ 1, 1,-1],[ 1,-1,-1], [-1,-1, 1],[-1, 1, 1],[ 1, 1, 1],[ 1,-1, 1] ] f = [ [0,1,2,3],[4,7,6,5],[0,3,7,4],[3,2,6,7],[2,1,5,6],[1,0,4,5]] e = [ [3,2],[2,1],[1,0],[0,3], [7,6],[6,5],[5,4],[4,7], [0,4],[3,7],[2,6],[1,5]] return np.array(v),np.array(f),np.array(e) def _preTriangulateDodoc(v,f,e,fc) : # (special Case) # Only used for rez=1 of a platonic dodecahedron. Each pentagon # is broken into 5 isoceles triangles from a centered vertex. subFaceIndices,subEdgeIndices = [],[] newCoors,newColor = [],[] nvInx = len(v) for i, face in enumerate(f) : for n0 in range(5) : n1 = (n0+1)%5 subFaceIndices.append( [face[n0],face[n1],nvInx+i] ) subEdgeIndices.append( [face[n0],nvInx+i] ) newCoors.append( np.sum(v[face], axis=0)/5 ) newColor += [fc[i]]*5 newverts = np.append(v,newCoors,axis=0) newfvInx = np.array(subFaceIndices) newEvInx = np.append(e,subEdgeIndices,axis=0) newColor = np.array(newColor) return newverts,newfvInx,newEvInx,newColor #..................................... if not isinstance(rez, int) : raise ValueError('Incorrect rez type, must ba of type int') if (rez<0) or (rez>_MAXREZ) : raise ValueError('Incorrest rez of {}, must be an int, 0 <= rez <= {}'.format(rez,_MAXREZ)) if basetype is None : basetype = SphericalSurface._default_base baseDict = { 'tetra':"tetrahedron",'octa':"octahedron", 'icosa':"icosahedron",'cube':"cube",'dodeca':"dodecahedron", 'cube_a':"cube_align" } baseCheck = baseDict.keys() if basetype not in baseCheck : raise ValueError('Basetype {} is not recognized. Possible values are: {}'.format(basetype,baseCheck) ) if basetype == 'cube_a' : surface = SphericalSurface(basetype='cube',name=name, **kwargs) else : surface = SphericalSurface(basetype=basetype,name=name, **kwargs) surface._rez = rez if basetype == 'cube_a' : v,f,e = fev_cube_a() surface.vertexCoor = v surface.fvIndices = f surface.evIndices = e surface.vfIndicesList = surface._vertexCommonFaceIndices() surface.efIndicesList = surface._edgeCommonFaceIndices() surface.set_verts( v[f] ) if basetype == 'cube' : v,f,e = SphericalSurface.get_cube() surface.vertexCoor = v surface.fvIndices = f surface.evIndices = e surface.vfIndicesList = surface._vertexCommonFaceIndices() surface.efIndicesList = surface._edgeCommonFaceIndices() surface.set_verts( v[f] ) if basetype == 'dodeca' : v,f,e = SphericalSurface.get_dodecahedron() if rez>0 : fc = surface._dfacecolors # update for v_1.2.0 v,f,e,fc = _preTriangulateDodoc(v,f,e,fc) surface.set_facecolor(fc) rez -= 1 surface.vertexCoor = v surface.fvIndices = f surface.evIndices = e surface.vfIndicesList = surface._vertexCommonFaceIndices() surface.efIndicesList = surface._edgeCommonFaceIndices() surface.set_verts( v[f] ) surface._postProc_surfaceColors(True) if rez > 0 : surface.triangulate(rez) surface._set_geometric_bounds() surface._basetype = basetype + '*' surface.name = baseDict[basetype] if name is None else name return surface
[docs] @staticmethod def get_dodecahedron() : """Numpy arrays of dodecahedron vertices (20,3) and face vertex indices (12,5) and edge vertex indices (30,2) """ v,f,e = SphericalSurface._get_platonic_solids_dictionary('dodeca') return np.array(v), np.array(f), np.array(e)
[docs] @staticmethod def get_cube() : """Numpy arrays of cube vertices (8,3) and face vertex indices 6,5) and edge vertex indices (12,2) """ v,f,e = SphericalSurface._get_platonic_solids_dictionary('cube') return np.array(v), np.array(f), np.array(e)
@staticmethod def _get_platonic_solids_dictionary(getArrays = None) : """Constructs the dictionary of base spherical surface geometries.""" # .................................................... def get_cube() : zeta = 1/3 x0 = np.sqrt(2)/3 y0 = x0*np.sqrt(3) chi = 2*x0 cubeBaseVert = [ [0,0,1], [chi,0,zeta], [-x0,y0,zeta], [-x0,-y0,zeta], [-chi,0,-zeta], [x0,-y0,-zeta], [x0,y0,-zeta], [0,0,-1] ] cubeBaseFaces=[ [0,1,6,2],[0,2,4,3],[0,3,5,1], [7,6,1,5],[7,4,2,6],[7,5,3,4] ] cubeBaseEdges = [ [0,1], [0,2], [0,3], [7,4], [7,5], [7,6], [1,6], [6,2], [2,4], [4,3], [3,5], [5,1] ] return cubeBaseVert, cubeBaseFaces, cubeBaseEdges def get_dodecahedron() : zeta = 1/3 ksi = np.sqrt(5)/3 lam_squ = 8/9 hEdge_squ = (1-ksi)/2 Ax = 2/3 Apx = 1/3 Bx = np.sqrt(lam_squ-hEdge_squ) Cx = ksi Dx = 1-Bx Apy = np.sqrt(3)/3 By = np.sqrt(hEdge_squ) Cy = Apy Dy = By + Cy # ----------------------- docTop = [ [0,0,1], [Ax, 0, ksi], [-Apx, Apy, ksi], [-Apx, -Apy, ksi], [Cx, Cy, zeta], [Dx, Dy, zeta], [-Bx, By, zeta], [-Bx, -By, zeta], [Dx, -Dy, zeta], [Cx, -Cy, zeta] ] docBottom = [] for ver in docTop : docBottom.append( np.multiply(ver,[-1,1,-1]).tolist() ) docbaseVert = np.concatenate((docTop,docBottom)).tolist() docbaseFaces = [ [0,1,4,5,2], [0,2,6,7,3] , [0,3,8,9,1], [1,9,17,16,4], [2,5,15,14,6], [3,7,19,18,8], [10,12,16,17,13], [10,11,14,15,12], [10,13,18,19,11], [11,19,7,6,14],[13,17,9,8,18],[12,15,5,4,16] ] docbaseEdges = [ [ 0, 1], [ 0, 2], [ 0, 3], [ 4, 5], [ 6, 7], [ 8, 9], [ 1, 9], [ 1, 4], [ 2, 5], [ 2, 6], [ 3, 7], [ 3, 8], [10,11], [10,12], [10,13], [14,15], [16,17], [18,19], [11,19], [11,14], [12,15], [12,16], [13,17], [13,18], [ 6,14], [ 4,16], [ 8,18], [ 5,15], [ 9,17], [ 7,19] ] return docbaseVert, docbaseFaces, docbaseEdges def construct_tetrahedron() : cSqr = (np.tan(np.pi/3))*(np.tan(np.pi/3)) zeta = (cSqr-1)/(2*cSqr) chi = np.sqrt(3*cSqr-1)*np.sqrt(cSqr+1)/(2*cSqr) x0 = chi*np.cos(np.pi/3) y0 = chi*np.sin(np.pi/3) baseVert = [ [0,0,1], [chi,0,-zeta], [-x0,y0,-zeta], [-x0,-y0,-zeta] ] baseFaces=([0,1,2],[0,2,3],[0,3,1],[1,3,2]) Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,0,2] } def construct_cube() : cubeBaseVert, cubeBaseFaces, cubeBaseEdges = get_cube() baseFaces = [] baseVert = cubeBaseVert.copy() for face in cubeBaseFaces : vcSum = [0,0,0] # get square face center for vI in face : vcSum = np.add(vcSum,cubeBaseVert[vI]) vcSum = np.divide(vcSum,4) vcCenter = vcSum/np.linalg.norm(vcSum) vIndex = len(baseVert) baseVert.append(vcCenter) # subdivide cube face into 4 triangles for i in range(0,4) : newFace = [] newFace.append(vIndex) newFace.append(face[i]) newFace.append(face[ (i+1)%4]) baseFaces.append(newFace) Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,0,2] } def construct_cubeS() : zeta = 1/3 x0 = np.sqrt(2)/3 y0 = x0*np.sqrt(3) chi = 2*x0 soff = 0.01 x0S, y0S = soff/2, soff*np.sqrt(3.0)/2.0 w = y0S/2. cubeBaseVert = [ [chi,-w,zeta], [chi, w,zeta], [-x0,y0,zeta], [-x0,-y0,zeta], [-chi,0,-zeta], [x0,-y0,-zeta], [x0,y0,-zeta], [ soff, w, 1], [ x0S, y0S, 1], [ -x0S, y0S, 1], [ -soff, 0, 1], [ -x0S,-y0S, 1], [ x0S,-y0S, 1], [ soff, -w, 1], [ soff, w,-1], [ x0S, y0S,-1], [ -x0S, y0S,-1], [ -soff, 0,-1], [ -x0S,-y0S,-1], [ x0S,-y0S,-1], [ soff, -w,-1] ] cubeBaseFaces=( [ 7, 1, 6, 8], [ 8, 6, 2, 9], [ 9, 2, 4,10], [10, 4, 3,11], [11, 3, 5,12], [12, 5, 0,13], [14,15, 6, 1], [15,16, 2, 6], [16,17, 4, 2], [17,18, 3, 4], [18,19, 5, 3], [19,20, 0, 5] ) baseFaces = [] baseVert = cubeBaseVert.copy() for face in cubeBaseFaces : vcSum = [0,0,0] # get square face center for vI in face : vcSum = np.add(vcSum,cubeBaseVert[vI]) vcSum = np.divide(vcSum,4) vcCenter = vcSum/np.linalg.norm(vcSum) vIndex = len(baseVert) baseVert.append(vcCenter) # subdivide cube face into 4 triangles for i in range(0,4) : newFace = [] newFace.append(vIndex) newFace.append(face[i]) newFace.append(face[ (i+1)%4]) baseFaces.append(newFace) Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,8,1] } def construct_octahedron(): baseVert = [ [0,0,1], [1,0,0], [0,1,0], [-1,0,0], [0,-1,0], [0,0,-1] ] baseFaces=([0,1,2],[0,2,3],[0,3,4],[0,4,1], [5,2,1],[5,3,2],[5,4,3],[5,1,4]) Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,0,2] } def construct_octahedronS(): soff = 0.01 baseVertS = [] x0S, y0S = soff/np.sqrt(2.0), soff/np.sqrt(2.0) baseVertS = [ [1,-y0S,0], [1, y0S,0], [0,1,0], [-1,0,0], [0,-1,0], [ x0S, y0S, 1], [-x0S, y0S, 1], [-x0S, -y0S, 1], [ x0S, -y0S, 1], [ x0S, y0S,-1], [-x0S, y0S,-1], [-x0S, -y0S,-1], [ x0S, -y0S,-1] ] baseFacesS=( [5,1,2], [6,2,3], [7,3,4], [8,4,0], [2,6,5], [3,7,6], [4,8,7], [9,2,1], [10,3,2], [11,4,3], [12,0,4], [2,9,10], [3,10,11], [4,11,12] ) FoS = len(baseFacesS) return { 'baseFaceIndices' : baseFacesS , 'baseVerticesCoor' : baseVertS, 'fevParam' : [FoS,5,1] } def construct_dodecahedron() : docbaseVert, docbaseFaces, docbaseEdges = get_dodecahedron() baseFaces = [] baseVert = docbaseVert.copy() for face in docbaseFaces : vcSum = [0,0,0] # get pentagon face center for vI in face : vcSum = np.add(vcSum,docbaseVert[vI]) vcSum = np.divide(vcSum,4) vcCenter = vcSum/np.linalg.norm(vcSum) vIndex = len(baseVert) baseVert.append(vcCenter) # subdivide pentagon face into 5 triangles for i in range(0,5) : newFace = [] newFace.append(vIndex) newFace.append(face[i]) newFace.append(face[ (i+1)%5]) baseFaces.append(newFace) Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,0,2] } def construct_icosahedron(): cSqr = (np.tan(np.pi/5))*(np.tan(np.pi/5)) zeta = (1-cSqr)/(2*cSqr) chi = np.sqrt(3*cSqr-1)*np.sqrt(cSqr+1)/(2*cSqr) thetaInc = 2*np.pi/5 thetaOff = thetaInc/2 baseVert = [] baseVert.append([0,0,1]) for i in range(0,5) : theta = i*thetaInc baseVert.append( [ chi*np.cos(theta), chi*np.sin(theta), zeta] ) for i in range(0,5) : theta = thetaOff+i*thetaInc baseVert.append( [ chi*np.cos(theta), chi*np.sin(theta), -zeta] ) baseVert.append([0,0,-1]) baseFaces=([0,1,2],[0,2,3],[0,3,4],[0,4,5],[0,5,1], [6,2,1],[7,3,2],[8,4,3],[9,5,4],[10,1,5], [2,6,7],[3,7,8],[4,8,9],[5,9,10],[1,10,6], [11,7,6],[11,8,7],[11,9,8],[11,10,9],[11,6,10]) Fo = len(baseFaces) return { 'baseFaceIndices' : baseFaces , 'baseVerticesCoor' : baseVert, 'fevParam' : [Fo,0,2] } # .................................................... if getArrays == 'cube' : return get_cube() if getArrays == 'dodeca' : return get_dodecahedron() surfacesDictionary = {} surfacesDictionary['tetra'] = construct_tetrahedron() surfacesDictionary['cube'] = construct_cube() surfacesDictionary['cube_s'] = construct_cubeS() surfacesDictionary['octa'] = construct_octahedron() surfacesDictionary['octa_s'] = construct_octahedronS() surfacesDictionary['dodeca'] = construct_dodecahedron() surfacesDictionary['icosa'] = construct_icosahedron() # ... 'special case' only for fev method for spherical surfaces. surfacesDictionary['octa_c'] = { 'fevParam' : [16,5,1] } surfacesDictionary['cube_c'] = { 'fevParam' : [12,4,1] } # ... 'special case' only for fev method for platonic surfaces. surfacesDictionary['cube_a'] = { 'fevParam' : [12,0,2] } surfacesDictionary['cube*'] = { 'fevParam' : [12,0,2] } surfacesDictionary['dodeca*'] = { 'fevParam' : [36,0,2] } return surfacesDictionary @staticmethod def _midVectorFun(vectA, vectB) : """Mid-point between two points on a unit sphere.""" mid = np.add(vectA,vectB) return mid/np.linalg.norm(mid) @staticmethod def _map3Dto2Dsquare(xyz) : """Override map surface to rectagular unit coordinates (0,1).""" x,y,z = xyz t = np.arctan2(y,x) theta = np.where( t<0, 2.0*np.pi + t ,t) phi = np.arcsin(z) a = theta/(2*np.pi) b =