Exemple #1
0
    def __init__(self,
                 lc,
                 F,
                 gradF,
                 hessF,
                 paramlist=[],
                 eps=1e-6,
                 iterations=10,
                 **kwargs):
        """
        Freeshape surface defined by abstract function F (either implicitly
        or explicitly) and its x, y, z derivatives
        :param F: explicit or implicit function in x, y (or z), paramslst
        :param gradF: closed form gradient in x, y, z, paramslst
        :param hessH: closed form Hessian in x, y, z, paramslst
        :param paramlist: [("param1", value), ("param2", value2), ...]
        :param eps: convergence parameter
        :param iterations: convergence parameter
        """

        super(FreeShape, self).__init__(lc, **kwargs)

        self.params = {}
        for (name, value) in paramlist:
            self.params[name] = OptimizableVariable(name=name, value=value)

        self.eps = eps
        self.iterations = iterations
        self.F = F  # implicit function in x, y, z, paramslst
        self.gradF = gradF  # closed form gradient in x, y, z, paramslst
        self.hessF = hessF  # closed form Hessian in x, y, z, paramslst
Exemple #2
0
    def __init__(self,
                 lc,
                 dllfile,
                 param_dict={},
                 xdata_dict={},
                 isWinDLL=False,
                 curv=0.0,
                 cc=0.0,
                 **kwargs):
        """
        param: lc LocalCoordinateSystem of shape
        param: dllfile (string) path to DLL file
        param: param_dict (dictionary) key: string, value: (int 0 to 8, float); initializes optimizable variables.
        param: xdata_dict (dictionary) key: string, value: (int  0 to 200, float); initializes optimizable variables.
        param: isWinDLL (bool): is DLL compiled in Windows or Linux?        
        
        Notice that this class only calls the appropriate functions from the DLL.
        The user is responsible to get all necessary functions running to use this DLL.
        (i.e. intersect, sag, normal, ...)
        
        To compile the DLL for Linux, remove all Windows and calling convention stuff an build it
        via:

            gcc -c -fpic -o us_stand.o us_stand.c -lm
            gcc -shared -o us_stand.so us_stand.o


        """
        super(ZMXDLLShape, self).__init__(lc, curv=curv, cc=cc, **kwargs)

        if isWinDLL:
            self.dll = ctypes.WinDLL(dllfile)
        else:
            self.dll = ctypes.CDLL(dllfile)

        self.param = {}
        for (key, (value_int, value_float)) in param_dict.iteritems():
            self.param[value_int] = OptimizableVariable(name="param" +
                                                        str(value_int),
                                                        value=value_float)
        self.xdata = {}
        for (key, (value_int, value_float)) in xdata_dict.iteritems():
            self.xdata[value_int] = OptimizableVariable(name="xdata" +
                                                        str(value_int),
                                                        value=value_float)
        self.us_surf = self.dll.UserDefinedSurface
Exemple #3
0
    def __init__(self, lc, curv=0.0, cc=0.0, **kwargs):
        """
        Create cylindric conic section surface.

        :param curv: Curvature of the surface (float).
        :param cc: Conic constant (float).

        -1 < cc < 0 oblate elliptic
             cc = 0 sphere
         0 < cc < 1 prolate elliptic
             cc = 1 parabolic
             cc > 1 hyperbolic
        """
        super(Cylinder, self).__init__(lc, **kwargs)

        self.curvature = OptimizableVariable(name="curvature", value=curv)
        self.conic = OptimizableVariable(name="conic constant", value=cc)
Exemple #4
0
    def __init__(self, lc, curv=0.0, cc=0.0, **kwargs):
        """
        Create rotationally symmetric surface
        with a conic cross section in the meridional plane.

        :param curv: Curvature of the surface (float).
        :param cc: Conic constant (float).

        -1 < cc < 0 oblate rotational ellipsoid
             cc = 0 sphere
         0 < cc < 1 prolate rotational ellipsoid
             cc = 1 rotational paraboloid
             cc > 1 rotational hyperboloid
        """
        super(Conic, self).__init__(lc, **kwargs)

        self.curvature = OptimizableVariable(name="curvature", value=curv)
        self.conic = OptimizableVariable(name="conic constant", value=cc)
Exemple #5
0
    def __init__(self, lc, curv=0.0, cc=0.0):
        """
        Create cylindric conic section surface.

        :param curv: Curvature of the surface (float).
        :param cc: Conic constant (float).

        -1 < cc < 0 oblate elliptic
             cc = 0 sphere
         0 < cc < 1 prolate elliptic
             cc = 1 parabolic
             cc > 1 hyperbolic
        """
        super(Cylinder, self).__init__(lc)

        self.curvature = OptimizableVariable("fixed", value=curv)
        self.addVariable(
            "curvature", self.curvature
        )  #self.createOptimizableVariable("curvature", value=curv, status=False)
        self.conic = OptimizableVariable("fixed", value=cc)
        self.addVariable(
            "conic constant", self.conic
        )  #self.createOptimizableVariable("conic constant", value=cc, status=False)
Exemple #6
0
class Conic(Shape):
    def __init__(self, lc, curv=0.0, cc=0.0, **kwargs):
        """
        Create rotationally symmetric surface
        with a conic cross section in the meridional plane.

        :param curv: Curvature of the surface (float).
        :param cc: Conic constant (float).

        -1 < cc < 0 oblate rotational ellipsoid
             cc = 0 sphere
         0 < cc < 1 prolate rotational ellipsoid
             cc = 1 rotational paraboloid
             cc > 1 rotational hyperboloid
        """
        super(Conic, self).__init__(lc, **kwargs)

        self.curvature = OptimizableVariable(name="curvature", value=curv)
        self.conic = OptimizableVariable(name="conic constant", value=cc)

    def getSag(self, x, y):
        """
        Return the sag of the surface mesured from the optical axis vertex.
        :param x: x coordinate on the surface (float or 1d numpy array of floats)
        :param y: y coordinate on the surface (float or 1d numpy array of floats)
        :return sag: (float or 1d numpy array of floats)
        """

        return self.conic_function(rsquared=x**2 + y**2)

    def conic_function(self, rsquared):
        """
        conic section function
        :param rsquared: distance from the optical axis (float or 1d numpy array of floats)
        :return z: sag (float or 1d numpy array of floats)
        """
        sqrtterm = 1 - (1 + self.conic.evaluate()
                        ) * self.curvature.evaluate()**2 * rsquared
        z = self.curvature.evaluate() * rsquared / (1 + np.sqrt(sqrtterm))

        return z

    def getGrad(self, x, y):
        """
        normal on a rotational symmetric conic section.
        :param x: x coordinates on the conic surface (float or 1d numpy array of floats)
        :param y: y coordinates on the conic surface (float or 1d numpy array of floats)
        :return normal: normal vectors ( 2d 3xN numpy array of floats )
        """
        z = self.getSag(x, y)

        curv = self.curvature.evaluate()
        cc = self.conic.evaluate()

        # gradient calculated from -1/2(1+cc)c z^2 + z -1/2 c (x^2 + y^2) = 0

        gradient = np.zeros((3, len(x)), dtype=float)
        gradient[0] = -curv * x
        gradient[1] = -curv * y
        gradient[2] = 1 - curv * z * (1 + cc)

        return gradient

    def getHessian(self, x, y):
        """
        Returns the local Hessian of a conic section (in vertex coordinates).
        :param x: x coordinates on the conic surface (float or 1d numpy array of floats)
        :param y: y coordinates on the conic surface (float or 1d numpy array of floats)
        :return hessian: Hessian in (3x3xN numpy array of floats)
        """
        # For the Hessian of a conic section there are no z values needed

        curv = self.curvature()
        cc = self.conic()
        (num_pts, ) = np.shape(x)

        hessian = np.array([[curv, 0, 0], [0, curv, 0],
                            [0, 0, curv * (1 + cc)]])
        hessian = np.repeat(hessian[:, :, np.newaxis], num_pts, axis=2)

        return hessian

    def getNormalDerivative(self, xveclocal):
        """
        :param xveclocal (3xN array float) 
        
        :return (3x3xN numpy array of float) 
        """

        (x, y, z) = (xveclocal[0], xveclocal[1], xveclocal[2])
        rho = self.curvature()
        cc = self.conic()

        (num_dims, num_points) = np.shape(xveclocal)

        factor = 1. - cc * rho**2 * (x**2 + y**2)

        curvmatrix = np.array([[rho, 0, 0], [0, rho, 0],
                               [0, 0, rho * (1. + cc)]])
        curvmatrix = np.repeat(curvmatrix[:, :, np.newaxis],
                               num_points,
                               axis=2)

        prematrix = 1. / np.sqrt(factor) * curvmatrix

        innermatrix = np.repeat(np.eye(3)[:, :, np.newaxis], num_points, axis=2) \
            - 1./factor*np.array([
                [rho**2*x**2, rho**2*x*y, rho*x*(rho*(1+cc)*z - 1.)],
                [rho**2*y*x, rho**2*y**2, rho*y*(rho*(1+cc)*z - 1.)],
                [rho*x*(rho*(1+cc)*z - 1.), rho*y*(rho*(1+cc)*z - 1.), 1 - rho**2*(1+cc)*(x**2 + y**2)]
            ])

        return np.einsum('ij...,jk...', prematrix, innermatrix).T

    def getCentralCurvature(self):
        return self.curvature.evaluate()

    def intersect(self, raybundle):
        """
        Calculates intersection from raybundle.
        
        :param raybundle (RayBundle object), gets changed!
        """

        (r0, rayDir) = self.getLocalRayBundleForIntersect(raybundle)

        # r0 is raybundle.o in the local coordinate system
        # rayDir = raybundle.rayDir in the local coordinate system
        # raybundle itself lives in the global coordinate system

        # FIXME: G = 0 if start points lie on a conic with the same parameters than
        # the next surface! (e.g.: water drop with internal reflection)

        F = rayDir[2] - self.curvature.evaluate() * (
            rayDir[0] * r0[0] + rayDir[1] * r0[1] + rayDir[2] * r0[2] *
            (1 + self.conic.evaluate()))
        G = self.curvature.evaluate() * (
            r0[0]**2 + r0[1]**2 + r0[2]**2 *
            (1 + self.conic.evaluate())) - 2 * r0[2]
        H = -self.curvature.evaluate() - self.conic.evaluate(
        ) * self.curvature.evaluate() * rayDir[2]**2

        square = F**2 + H * G
        division_part = F + np.sqrt(square)

        #H_nearly_zero = (np.abs(H) < numerical_tolerance)
        #G_nearly_zero = (np.abs(G) < numerical_tolerance)
        F_nearly_zero = (np.abs(F) < numerical_tolerance)
        #t = np.where(H_nearly_zero, G/(2.*F), np.where(G_nearly_zero, -2.*F/H, G / division_part))

        t = G / division_part

        intersection = r0 + rayDir * t

        # find indices of rays that don't intersect with the sphere
        validIndices = square > 0  #*(True - F_nearly_zero)

        globalinter = self.lc.returnLocalToGlobalPoints(intersection)

        raybundle.append(globalinter, raybundle.k[-1], raybundle.Efield[-1],
                         validIndices)
Exemple #7
0
    def __init__(self, name="", **kwargs):
        # TODO: Reference to global to be rewritten into reference to root
        '''
        Defines a local coordinate system, on which translated or tilted optical surfaces may refer.

        @param: name -- name of the coordinate system for identification (str)
                        if the value is "", a uuid will be generated as name. 
        @param: kwargs -- keyword args: 
                        decx, decy, decz: decenter of the surface in x, y and z, respectively (float)
                                          Default values are zeros.
                                          decz denotes the position of the current surface.
                                          decz is equivalent to the Zemax thickness value CTVA or THIC of the previous surface.
                        tiltx, tilty, tiltz: tilt around x, y, or z axis in radians (float).
                                          Default values are zeros.
                        tiltThenDecenter: order of tilt and decenter operation (bool or int).
                                          Default value is zero.
                                          0 or False means: the decenter operations are performed first, then tiltx, then tilty, then tiltz.
                                          1 or True means: tiltz first, then tilty, then tiltx, then decenter.       
                        observers:        list of observers derived from AbstractObserver
        '''
        super(LocalCoordinates, self).__init__(name=name, **kwargs)        

        
        (decz, decx, decy, tiltx, tilty, tiltz, tiltThenDecenter) = \
        (kwargs.get(key, 0.0) for key in ["decz", "decx", "decy", "tiltx", "tilty", "tiltz", "tiltThenDecenter"])

        self.list_observers = kwargs.get("observers", [])        
                
        

        self.decx = OptimizableVariable(name="decx", variable_type='fixed', value=decx)
        self.decy = OptimizableVariable(name="decy", variable_type='fixed', value=decy)
        self.decz = OptimizableVariable(name="decz", variable_type='fixed', value=decz)
        self.tiltx = OptimizableVariable(name="tiltx", variable_type='fixed', value=tiltx)
        self.tilty = OptimizableVariable(name="tilty", variable_type='fixed', value=tilty)
        self.tiltz = OptimizableVariable(name="tiltz", variable_type='fixed', value=tiltz)
       
        self.tiltThenDecenter = tiltThenDecenter
        
        self.parent = None # None means reference to root coordinate system 
        self.__children = [] # children
    
        self.globalcoordinates = np.array([0, 0, 0])
        self.localdecenter = np.array([0, 0, 0])
        self.localrotation = np.lib.eye(3)
        self.localbasis = np.lib.eye(3)

        self.update() # initial update
Exemple #8
0
class LocalCoordinates(ClassWithOptimizableVariables):
    def __init__(self, name="", **kwargs):
        # TODO: Reference to global to be rewritten into reference to root
        '''
        Defines a local coordinate system, on which translated or tilted optical surfaces may refer.

        @param: name -- name of the coordinate system for identification (str)
                        if the value is "", a uuid will be generated as name. 
        @param: kwargs -- keyword args: 
                        decx, decy, decz: decenter of the surface in x, y and z, respectively (float)
                                          Default values are zeros.
                                          decz denotes the position of the current surface.
                                          decz is equivalent to the Zemax thickness value CTVA or THIC of the previous surface.
                        tiltx, tilty, tiltz: tilt around x, y, or z axis in radians (float).
                                          Default values are zeros.
                        tiltThenDecenter: order of tilt and decenter operation (bool or int).
                                          Default value is zero.
                                          0 or False means: the decenter operations are performed first, then tiltx, then tilty, then tiltz.
                                          1 or True means: tiltz first, then tilty, then tiltx, then decenter.       
                        observers:        list of observers derived from AbstractObserver
        '''
        super(LocalCoordinates, self).__init__(name=name, **kwargs)        

        
        (decz, decx, decy, tiltx, tilty, tiltz, tiltThenDecenter) = \
        (kwargs.get(key, 0.0) for key in ["decz", "decx", "decy", "tiltx", "tilty", "tiltz", "tiltThenDecenter"])

        self.list_observers = kwargs.get("observers", [])        
                
        

        self.decx = OptimizableVariable(name="decx", variable_type='fixed', value=decx)
        self.decy = OptimizableVariable(name="decy", variable_type='fixed', value=decy)
        self.decz = OptimizableVariable(name="decz", variable_type='fixed', value=decz)
        self.tiltx = OptimizableVariable(name="tiltx", variable_type='fixed', value=tiltx)
        self.tilty = OptimizableVariable(name="tilty", variable_type='fixed', value=tilty)
        self.tiltz = OptimizableVariable(name="tiltz", variable_type='fixed', value=tiltz)
       
        self.tiltThenDecenter = tiltThenDecenter
        
        self.parent = None # None means reference to root coordinate system 
        self.__children = [] # children
    
        self.globalcoordinates = np.array([0, 0, 0])
        self.localdecenter = np.array([0, 0, 0])
        self.localrotation = np.lib.eye(3)
        self.localbasis = np.lib.eye(3)

        self.update() # initial update


    def getChildren(self):
        return self.__children
        
    children = property(getChildren)


    def addChild(self, childlc):
        """
        Add a child coordinate system. 
        That is, the child coordinate system tilt and decenter 
        are defined relative to the current system, "self".

        @param: childlc -- the local coordinate system (object)
        
        @return: childlc -- return the input argument (object)
        """        
        childlc.parent = self
        childlc.update()
        self.__children.append(childlc)
        return childlc
        
    def addChildToReference(self, refname, childlc):
        """
        Adds a child to the coordinate system specified.

        @param: refname -- name of the desired parent of childlc (str)
                           if name does not occur in self or its (grand)-children, nothing is done.
        @param: childlc -- the coordinate system looking for a new parent (object)

        @return: childlc -- return the input argument (object)

        TODO: refnames occuring twice may lead to undefined behavior.
        """
        if self.name == refname:
            self.addChild(childlc)
        else:
            for x in self.__children:
                x.addChildToReference(refname, childlc)
        return childlc
    

    
    def calculateMatrixFromTilt(self, tiltx, tilty, tiltz, tiltThenDecenter=0):
        if tiltThenDecenter == 0:
            res = np.dot(rodrigues(tiltz, [0, 0, 1]), np.dot(rodrigues(tilty, [0, 1, 0]), rodrigues(tiltx, [1, 0, 0])))
        else:
            res = np.dot(rodrigues(tiltx, [1, 0, 0]), np.dot(rodrigues(tilty, [0, 1, 0]), rodrigues(tiltz, [0, 0, 1])))
        return res
        
    def FactorMatrixXYZ(self, mat):
        ''' 
        R = Rx(thetax) Ry(thetay) Rz(thetaz). 
        According to www.geometrictools.com/Documentation/EulerAngles.pdf
        section 2.1. October 2016.
        '''
        thetax = thetay = thetaz = 0
        
        if mat[0, 2] < 1:
            if mat[0, 2] > -1:
                thetay = math.asin(mat[0, 2])
                thetax = math.atan2(-mat[1, 2], mat[2, 2])
                thetaz = math.atan2(-mat[0, 1], mat[0, 0])
            else:
                thetay = -math.pi/2
                thetax = -math.atan2(mat[1, 0], mat[1, 1])
                thetaz = 0.
        else:
            thetay = math.pi/2
            thetax = math.atan2(mat[1, 0], mat[1, 1])
            thetaz = 0.
        
        return (thetax, thetay, thetaz)
                
            
    
    def FactorMatrixZYX(self, mat):
        ''' 
        R = Rz(thetaz) Ry(thetay) Rx(thetax). 
        According to www.geometrictools.com/Documentation/EulerAngles.pdf
        section 2.6. October 2016.
        '''
        thetax = thetay = thetaz = 0
        
        if mat[2, 0] < 1:
            if mat[2, 0] > -1:
                thetay = math.asin(-mat[2, 0])
                thetaz = math.atan2(mat[1, 0], mat[0, 0])
                thetax = math.atan2(mat[2, 1], mat[2, 2])
            else:
                thetay = math.pi/2
                thetaz = -math.atan2(-mat[1, 2], mat[1, 1])
                thetax = 0.
        else:
            thetay = -math.pi/2
            thetaz = math.atan2(-mat[1, 2], mat[1, 1])
            thetax = 0.
        
        return (thetax, thetay, thetaz)

        
    
    def calculateTiltFromMatrix(self, mat, tiltThenDecenter=0):
        res = (0., 0., 0.)
        if tiltThenDecenter==0:
            res = self.FactorMatrixZYX(mat)
        else:
            res = self.FactorMatrixXYZ(mat)
        return res
            
            
    
    def calculate(self):
 
        """
        tiltThenDecenter=0: decx, decy, decz, tiltx, tilty, tiltz
        tiltThenDecenter=1: tiltx, tilty, tilty, decx, decy, decz
        
        notice negative signs for angles to make sure that for tiltx>0 the
        optical points in positive y-direction although the x-axis of the
        local coordinate system points INTO the screen
        This leads also to a clocking in the mathematical negative direction
        """
        tiltx = self.tiltx.evaluate()
        tilty = self.tilty.evaluate()
        tiltz = self.tiltz.evaluate()
        decx = self.decx.evaluate()
        decy = self.decy.evaluate()        
        decz = self.decz.evaluate()
        
        self.localdecenter = np.array([decx, decy, decz])
        self.localrotation = self.calculateMatrixFromTilt(tiltx, tilty, tiltz, self.tiltThenDecenter)

    def update(self):
        ''' 
        runs through all references specified and sums up 
        coordinates and local rotations to get appropriate 
        global coordinate
        '''
        self.calculate()

        parentcoordinates = np.array([0, 0, 0])
        parentbasis = np.lib.eye(3)

        if self.parent != None:        
            parentcoordinates = self.parent.globalcoordinates
            parentbasis = self.parent.localbasis
        
        self.localbasis = np.dot(parentbasis, self.localrotation)
        if self.tiltThenDecenter == 0:
            # first decenter then rotation
            self.globalcoordinates = \
            parentcoordinates + \
            np.dot(parentbasis, self.localdecenter)
            # TODO: removed .T on parentbasis to obtain correct behavior; examine!
        else:
            # first rotation then decenter
            self.globalcoordinates = \
            parentcoordinates + \
            np.dot(self.localbasis, self.localdecenter)
            # TODO: removed .T on localbasis to obtain correct behavior; examine!
        
        for ch in self.__children:
            ch.update()

        # inform observers about update
        self.informObservers()

    def aimAt(self, anotherlc, update=False):
        (tiltx, tilty, tiltz) = self.calculateAim(anotherlc)

        self.tiltx.setvalue(tiltx)        
        self.tilty.setvalue(tilty)        
        self.tiltz.setvalue(tiltz)        

        if update:
            self.update()

            
    def calculateAim(self, anotherlc):

        rotationtransform = np.zeros((3, 3))
        direction = self.returnGlobalToLocalPoints(anotherlc.globalcoordinates)
        dist = np.linalg.norm(direction)
        localzaxis = direction/dist
       
        #zaxis = normal(At - Eye)
        #xaxis = normal(cross(Up, zaxis))
        #yaxis = cross(zaxis, xaxis)        
        
        up = np.array([0, 1, 0]) # y-axis

        localxaxis = np.cross(up, localzaxis)
        localxaxis = localxaxis/np.linalg.norm(localxaxis)
        localyaxis = np.cross(localzaxis, localxaxis)
        localyaxis = localyaxis/np.linalg.norm(localyaxis)
        
        rotationtransform[:, 0] = localxaxis
        rotationtransform[:, 1] = localyaxis
        rotationtransform[:, 2] = localzaxis
        
        transformedlocalrotation = np.dot(rotationtransform, self.localrotation)
        
        (tiltx, tilty, tiltz) = self.calculateTiltFromMatrix(transformedlocalrotation, self.tiltThenDecenter)

        # seems to be only correct for
        # -pi/2 < tilty < pi/2
        # 0 < tiltx < pi
        # 0 < tiltz < pi        
        
        return (tiltx, tilty, tiltz)
        
    def returnActualToOtherPoints(self, localpts, lcother):
        # TODO: constraint: lcother and self share same root, check: lcother=self
        globalpts = self.returnLocalToGlobalPoints(localpts)
        return lcother.returnGlobalToLocalPoints(globalpts)

    def returnOtherToActualPoints(self, otherpts, lcother):
        # TODO: constraint: lcother and self share same root        
        globalpts = lcother.returnLocalToGlobalPoints(otherpts)
        return self.returnGlobalToLocalPoints(globalpts)
       
    def returnActualToOtherDirections(self, localdirs, lcother):
        # TODO: constraint: lcother and self share same root
        globaldirs = self.returnLocalToGlobalDirections(localdirs)
        return lcother.returnGlobalToLocalDirections(globaldirs)

    def returnOtherToActualDirections(self, otherdirs, lcother):
        globaldirs = lcother.returnLocalToGlobalDirections(otherdirs)
        return self.returnGlobalToLocalDirections(globaldirs)
        
    def returnActualToOtherTensors(self, localtensors, lcother):
        # TODO: constraint: lcother and self share same root
        globaltensors = self.returnLocalToGlobalTensorss(localtensors)
        return lcother.returnGlobalToLocalDirections(globaltensors)

    def returnOtherToActualTensors(self, othertensors, lcother):
        globaltensors = lcother.returnLocalToGlobalTensors(othertensors)
        return self.returnGlobalToLocalTensors(globaltensors)


    def returnLocalToGlobalPoints(self, localpts):
        """
        @param: localpts (3xN numpy array)
        @return: globalpts (3xN numpy array)
        """
        transformedlocalpts = np.dot(self.localbasis, localpts)
        # construction to use broadcasting        
        globalpts = (transformedlocalpts.T + self.globalcoordinates).T
        return globalpts

    def returnLocalToGlobalDirections(self, localdirs):
        """
        @param: localdirs (3xN numpy array)
        @return: globaldirs (3xN numpy array)
        """
        return np.dot(self.localbasis, localdirs)

    def returnGlobalToLocalPoints(self, globalpts):
        """
        @param: globalpts (3xN numpy array)
        @return: localpts (3xN numpy array)
        """
        translatedglobalpts = (globalpts.T - self.globalcoordinates).T
        # construction to use broadcasting
        localpts = np.dot(self.localbasis.T, translatedglobalpts)        
        return localpts

    def returnGlobalToLocalDirections(self, globaldirs):
        """
        @param: globaldirs (3xN numpy array)
        @return: localdirs (3xN numpy array)
        """
        
        localpts = np.dot(self.localbasis.T, globaldirs)        
        return localpts
        
    def returnGlobalToLocalTensors(self, globaltensor):
        """
        @param: globaldirs (3x3xN numpy array)
        @return: localdirs (3x3xN numpy array)
        """

        (num_dims_r, num_dims_c, num_pts) = np.shape(globaltensor)
        localbasisT = np.repeat(self.localbasis.T[:, :, np.newaxis], num_pts, axis=2)
        localtensor = np.einsum('lj...,ji...,ki...', localbasisT, globaltensor, localbasisT).T
        return localtensor

    def returnLocalToGlobalTensors(self, localtensor):
        """
        @param: globaldirs (3x3xN numpy array)
        @return: localdirs (3x3xN numpy array)
        """

        (num_dims_r, num_dims_c, num_pts) = np.shape(localtensor)
        localbasis = np.repeat(self.localbasis[:, :, np.newaxis], num_pts, axis=2)
        globaltensor = np.einsum('lj...,ji...,ki...', localbasis, localtensor, localbasis).T
        return globaltensor
        

    def returnConnectedNames(self):
        lst = [self.name]
        for ch in self.__children:
            lst = lst + ch.returnConnectedNames()
        return lst

    def returnConnectedChildren(self):
        lst = [self]
        for ch in self.__children:
            lst = lst + ch.returnConnectedChildren()
        return lst

        
    def pprint(self, n=0):
        """
        returns a string visualizing the tree structure of self and its children.

        @param n: indentation level (int)

        @return s: structure of self.name and its children (str)
        """
        s = n*"    " + self.name + " (" + str(self.globalcoordinates) + ")\n"
        for x in self.__children:
            s += x.pprint(n+1)
            
        return s
        

    def __str__(self):
        s = 'name \'%s\'\ntiltThenDecenter %d\nglobal coordinates: %s\nld: %s\nlr:\n%s\nlb:\n%s\nchildren %s'\
        % (self.name, self.tiltThenDecenter, \
        self.globalcoordinates, \
        self.localdecenter, \
        self.localrotation, \
        self.localbasis, \
        [i.name for i in self.__children])
        return s