def latticeVectors(lparms, tag="cubic", radians=False, debug=False): """ Generates direct and reciprocal lattice vector components in a crystal-relative RHON basis, X. The convention for fixing X to the lattice is such that a || x1 and c* || x3, where a and c* are direct and reciprocal lattice vectors, respectively. USAGE: lattice = LatticeVectors(lparms, <symmTag>) INPUTS: 1) lparms (1 x n float list) is the array of lattice parameters, where n depends on the symmetry group (see below). 2) symTag (string) is a case-insensitive string representing the symmetry type of the implied Laue group. The 11 available choices are shown below. The default value is 'cubic'. Note that each group expects a lattice parameter array of the indicated length and order. latticeType lparms ----------- ------------ 'cubic' a 'hexagonal' a, c 'trigonal' a, c 'rhombohedral' a, alpha (in degrees) 'tetragonal' a, c 'orthorhombic' a, b, c 'monoclinic' a, b, c, beta (in degrees) 'triclinic' a, b, c, alpha, beta, gamma (in degrees) OUTPUTS: 1) lattice is a dictionary containing the following keys/items: F (3, 3) double array transformation matrix taking componenents in the direct lattice (i.e. {uvw}) to the reference, X B (3, 3) double array transformation matrix taking componenents in the reciprocal lattice (i.e. {hkl}) to X BR (3, 3) double array transformation matrix taking componenents in the reciprocal lattice to the Fable reference frame (see notes) U0 (3, 3) double array transformation matrix (orthogonal) taking componenents in the Fable reference frame to X vol double the unit cell volume dparms (6, ) double list the direct lattice parameters: [a b c alpha beta gamma] rparms (6, ) double list the reciprocal lattice parameters: [a* b* c* alpha* beta* gamma*] NOTES: *) The conventions used for assigning a RHON basis, X -> {x1, x2, x3}, to each point group are consistent with those published in Appendix B of [1]. Namely: a || x1 and c* || x3. This differs from the convention chosen by the Fable group, where a* || x1 and c || x3 [2]. *) The unit cell angles are defined as follows: alpha=acos(b'*c/|b||c|), beta=acos(c'*a/|c||a|), and gamma=acos(a'*b/|a||b|). *) The reciprocal lattice vectors are calculated using the crystallographic convention, where the prefactor of 2*pi is omitted. In this convention, the reciprocal lattice volume is 1/V. *) Several relations from [3] were employed in the component calculations. REFERENCES: [1] J. F. Nye, ``Physical Properties of Crystals: Their Representation by Tensors and Matrices''. Oxford University Press, 1985. ISBN 0198511655 [2] E. M. Lauridsen, S. Schmidt, R. M. Suter, and H. F. Poulsen, ``Tracking: a method for structural characterization of grains in powders or polycrystals''. J. Appl. Cryst. (2001). 34, 744--750 [3] R. J. Neustadt, F. W. Cagle, Jr., and J. Waser, ``Vector algebra and the relations between direct and reciprocal lattice quantities''. Acta Cryst. (1968), A24, 247--248 """ # build index for sorting out lattice parameters lattStrings = [ "cubic", "hexagonal", "trigonal", "rhombohedral", "tetragonal", "orthorhombic", "monoclinic", "triclinic", ] if radians: angConv = 1.0 else: angConv = pi / 180.0 # degToRad deg90 = pi / 2.0 deg120 = 2.0 * pi / 3.0 # if tag == lattStrings[0]: # cubic cellparms = num.r_[num.tile(lparms[0], (3,)), deg90 * num.ones((3,))] elif tag == lattStrings[1] or tag == lattStrings[2]: # hexagonal | trigonal (hex indices) cellparms = num.r_[lparms[0], lparms[0], lparms[1], deg90, deg90, deg120] elif tag == lattStrings[3]: # rhombohedral cellparms = num.r_[num.tile(lparms[0], (3,)), num.tile(angConv * lparms[1], (3,))] elif tag == lattStrings[4]: # tetragonal cellparms = num.r_[lparms[0], lparms[0], lparms[1], deg90, deg90, deg90] elif tag == lattStrings[5]: # orthorhombic cellparms = num.r_[lparms[0], lparms[1], lparms[2], deg90, deg90, deg90] elif tag == lattStrings[6]: # monoclinic cellparms = num.r_[lparms[0], lparms[1], lparms[2], deg90, angConv * lparms[3], deg90] elif tag == lattStrings[7]: # triclinic cellparms = num.r_[ lparms[0], lparms[1], lparms[2], angConv * lparms[3], angConv * lparms[4], angConv * lparms[5] ] # fixed DP 2/24/16 else: raise RuntimeError("lattice tag '%s' is not recognized" % (tag)) if debug: print str(cellparms[0:3]) + " " + str(r2d * cellparms[3:6]) alfa = cellparms[3] beta = cellparms[4] gama = cellparms[5] cosalfar, sinalfar = cosineXform(alfa, beta, gama) a = cellparms[0] * num.r_[1, 0, 0] b = cellparms[1] * num.r_[num.cos(gama), num.sin(gama), 0] c = cellparms[2] * num.r_[num.cos(beta), -cosalfar * num.sin(beta), sinalfar * num.sin(beta)] ad = num.sqrt(sum(a ** 2)) bd = num.sqrt(sum(b ** 2)) cd = num.sqrt(sum(c ** 2)) # Cell volume V = num.dot(a, num.cross(b, c)) # F takes components in the direct lattice to X F = num.c_[a, b, c] # Reciprocal lattice vectors astar = num.cross(b, c) / V bstar = num.cross(c, a) / V cstar = num.cross(a, b) / V # and parameters ar = num.sqrt(sum(astar ** 2)) br = num.sqrt(sum(bstar ** 2)) cr = num.sqrt(sum(cstar ** 2)) alfar = num.arccos(num.dot(bstar, cstar) / br / cr) betar = num.arccos(num.dot(cstar, astar) / cr / ar) gamar = num.arccos(num.dot(astar, bstar) / ar / br) # B takes components in the reciprocal lattice to X B = num.c_[astar, bstar, cstar] cosalfar2, sinalfar2 = cosineXform(alfar, betar, gamar) afable = ar * num.r_[1, 0, 0] bfable = br * num.r_[num.cos(gamar), num.sin(gamar), 0] cfable = cr * num.r_[num.cos(betar), -cosalfar2 * num.sin(betar), sinalfar2 * num.sin(betar)] BR = num.c_[afable, bfable, cfable] U0 = num.dot(B, num.linalg.inv(BR)) if outputDegrees: dparms = num.r_[ad, bd, cd, r2d * num.r_[alfa, beta, gama]] rparms = num.r_[ar, br, cr, r2d * num.r_[alfar, betar, gamar]] else: dparms = num.r_[ad, bd, cd, num.r_[alfa, beta, gama]] rparms = num.r_[ar, br, cr, num.r_[alfar, betar, gamar]] L = {"F": F, "B": B, "BR": BR, "U0": U0, "vol": V, "dparms": dparms, "rparms": rparms} return L
def getFriedelPair(tth0, eta0, *ome0, **kwargs): """ Get the diffractometer angular coordinates in degrees for the Friedel pair of a given reflection (min angular distance). AUTHORS: J. V. Bernier -- 10 Nov 2009 USAGE: ome1, eta1 = getFriedelPair(tth0, eta0, *ome0, display=False, units='degrees', convention='hexrd') INPUTS: 1) tth0 is a list (or ndarray) of 1 or n the bragg angles (2theta) for the n reflections (tiled to match eta0 if only 1 is given). 2) eta0 is a list (or ndarray) of 1 or n azimuthal coordinates for the n reflections (tiled to match tth0 if only 1 is given). 3) ome0 is a list (or ndarray) of 1 or n reference oscillation angles for the n reflections (denoted omega in [1]). This argument is optional. 4) Keyword arguments may be one of the following: Keyword Values|{default} Action -------------- -------------- -------------- 'display' True|{False} toggles display info to cmd line 'units' 'radians'|{'degrees'} sets units for input angles 'convention' 'fable'|{'hexrd'} sets conventions defining the angles (see below) 'chiTilt' None the inclination (about Xlab) of the oscillation axis OUTPUTS: 1) ome1 contains the oscialltion angle coordinates of the Friedel pairs associated with the n input reflections, relative to ome0 (i.e. ome1 = <result> + ome0). Output is in DEGREES! 2) eta1 contains the azimuthal coordinates of the Friedel pairs associated with the n input reflections. Output units are controlled via the module variable 'outputDegrees' NOTES: JVB) The ouputs ome1, eta1 are written using the selected convention, but the units are alway degrees. May change this to work with Nathan's global... JVB) In the 'fable' convention [1], {XYZ} form a RHON basis where X is downstream, Z is vertical, and eta is CCW with +Z defining eta = 0. JVB) In the 'hexrd' convention [2], {XYZ} form a RHON basis where Z is upstream, Y is vertical, and eta is CCW with +X defining eta = 0. REFERENCES: [1] E. M. Lauridsen, S. Schmidt, R. M. Suter, and H. F. Poulsen, ``Tracking: a method for structural characterization of grains in powders or polycrystals''. J. Appl. Cryst. (2001). 34, 744--750 [2] J. V. Bernier, M. P. Miller, J. -S. Park, and U. Lienert, ``Quantitative Stress Analysis of Recrystallized OFHC Cu Subject to Deformed In Situ'', J. Eng. Mater. Technol. (2008). 130. DOI:10.1115/1.2870234 """ dispFlag = False fableFlag = False chi = None c1 = 1.0 c2 = pi / 180.0 zTol = 1.0e-7 # cast to arrays (in case they aren't) if num.isscalar(eta0): eta0 = [eta0] if num.isscalar(tth0): tth0 = [tth0] if num.isscalar(ome0): ome0 = [ome0] eta0 = num.asarray(eta0) tth0 = num.asarray(tth0) ome0 = num.asarray(ome0) if eta0.ndim != 1: raise RuntimeError, "your azimuthal input was not 1-D, so I do not know what you expect me to do" npts = len(eta0) if tth0.ndim != 1: raise RuntimeError, "your Bragg angle input was not 1-D, so I do not know what you expect me to do" else: if len(tth0) != npts: if len(tth0) == 1: tth0 = tth0 * num.ones(npts) elif npts == 1: npts = len(tth0) eta0 = eta0 * num.ones(npts) else: raise RuntimeError, "the azimuthal and Bragg angle inputs are inconsistent" if len(ome0) == 0: ome0 = num.zeros(npts) # dummy ome0 elif len(ome0) == 1 and npts > 1: ome0 = ome0 * num.ones(npts) else: if len(ome0) != npts: raise RuntimeError( "your oscialltion angle input is inconsistent; " + "it has length %d while it should be %d" % (len(ome0), npts) ) # keyword args processing kwarglen = len(kwargs) if kwarglen > 0: argkeys = kwargs.keys() for i in range(kwarglen): if argkeys[i] == "display": dispFlag = kwargs[argkeys[i]] elif argkeys[i] == "convention": if kwargs[argkeys[i]].lower() == "fable": fableFlag = True elif argkeys[i] == "units": if kwargs[argkeys[i]] == "radians": c1 = 180.0 / pi c2 = 1.0 elif argkeys[i] == "chiTilt": if kwargs[argkeys[i]] is not None: chi = kwargs[argkeys[i]] # a little talkback... if dispFlag: if fableFlag: print "\nUsing Fable angle convention\n" else: print "\nUsing image-based angle convention\n" # mapped eta input # - in DEGREES, thanks to c1 eta0 = mapAngle(c1 * eta0, [-180, 180], units="degrees") if fableFlag: eta0 = 90 - eta0 # must put args into RADIANS # - eta0 is in DEGREES, # - the others are in whatever was entered, hence c2 eta0 = d2r * eta0 tht0 = c2 * tth0 / 2 if chi is not None: chi = c2 * chi else: chi = 0 # --------------------- # SYSTEM SOLVE # # # cos(chi)cos(eta)cos(theta)sin(x) - cos(chi)sin(theta)cos(x) = sin(theta) - sin(chi)sin(eta)cos(theta) # # # Identity: a sin x + b cos x = sqrt(a**2 + b**2) sin (x + alfa) # # / # | atan(b/a) for a > 0 # alfa < # | pi + atan(b/a) for a < 0 # \ # # => sin (x + alfa) = c / sqrt(a**2 + b**2) # # must use both branches for sin(x) = n: x = u (+ 2k*pi) | x = pi - u (+ 2k*pi) # cchi = num.cos(chi) schi = num.sin(chi) ceta = num.cos(eta0) seta = num.sin(eta0) ctht = num.cos(tht0) stht = num.sin(tht0) nchi = num.c_[0.0, cchi, schi].T gHat0_l = -num.vstack([ceta * ctht, seta * ctht, stht]) a = cchi * ceta * ctht b = -cchi * stht c = stht + schi * seta * ctht # form solution abMag = num.sqrt(a * a + b * b) assert num.all(abMag > 0), "Beam vector specification is infeasible!" phaseAng = num.arctan2(b, a) rhs = c / abMag rhs[abs(rhs) > 1.0] = num.nan rhsAng = num.arcsin(rhs) # write ome angle output arrays (NaNs persist here) ome1 = rhsAng - phaseAng ome2 = num.pi - rhsAng - phaseAng ome1 = mapAngle(ome1, [-num.pi, num.pi], units="radians") ome2 = mapAngle(ome2, [-num.pi, num.pi], units="radians") ome_stack = num.vstack([ome1, ome2]) min_idx = num.argmin(abs(ome_stack), axis=0) ome_min = ome_stack[min_idx, range(len(ome1))] eta_min = num.nan * num.ones_like(ome_min) # mark feasible reflections goodOnes = -num.isnan(ome_min) numGood = sum(goodOnes) tmp_eta = num.empty(numGood) tmp_gvec = gHat0_l[:, goodOnes] for i in range(numGood): come = num.cos(ome_min[goodOnes][i]) some = num.sin(ome_min[goodOnes][i]) rchi = rotMatOfExpMap(num.tile(ome_min[goodOnes][i], (3, 1)) * nchi) gHat_l = num.dot(rchi, tmp_gvec[:, i].reshape(3, 1)) tmp_eta[i] = num.arctan2(gHat_l[1], gHat_l[0]) pass eta_min[goodOnes] = tmp_eta # everybody back to DEGREES! # - ome1 is in RADIANS here # - convert and put into [-180, 180] ome1 = mapAngle(mapAngle(r2d * ome_min, [-180, 180], units="degrees") + c1 * ome0, [-180, 180], units="degrees") # put eta1 in [-180, 180] eta1 = mapAngle(r2d * eta_min, [-180, 180], units="degrees") if not outputDegrees: ome1 = d2r * ome1 eta1 = d2r * eta1 return ome1, eta1
def latticeVectors(lparms, tag='cubic', radians=False, debug=False): """ Generates direct and reciprocal lattice vector components in a crystal-relative RHON basis, X. The convention for fixing X to the lattice is such that a || x1 and c* || x3, where a and c* are direct and reciprocal lattice vectors, respectively. USAGE: lattice = LatticeVectors(lparms, <symmTag>) INPUTS: 1) lparms (1 x n float list) is the array of lattice parameters, where n depends on the symmetry group (see below). 2) symTag (string) is a case-insensitive string representing the symmetry type of the implied Laue group. The 11 available choices are shown below. The default value is 'cubic'. Note that each group expects a lattice parameter array of the indicated length and order. latticeType lparms ----------- ------------ 'cubic' a 'hexagonal' a, c 'trigonal' a, c 'rhombohedral' a, alpha (in degrees) 'tetragonal' a, c 'orthorhombic' a, b, c 'monoclinic' a, b, c, beta (in degrees) 'triclinic' a, b, c, alpha, beta, gamma (in degrees) OUTPUTS: 1) lattice is a dictionary containing the following keys/items: F (3, 3) double array transformation matrix taking componenents in the direct lattice (i.e. {uvw}) to the reference, X B (3, 3) double array transformation matrix taking componenents in the reciprocal lattice (i.e. {hkl}) to X BR (3, 3) double array transformation matrix taking componenents in the reciprocal lattice to the Fable reference frame (see notes) U0 (3, 3) double array transformation matrix (orthogonal) taking componenents in the Fable reference frame to X vol double the unit cell volume dparms (6, ) double list the direct lattice parameters: [a b c alpha beta gamma] rparms (6, ) double list the reciprocal lattice parameters: [a* b* c* alpha* beta* gamma*] NOTES: *) The conventions used for assigning a RHON basis, X -> {x1, x2, x3}, to each point group are consistent with those published in Appendix B of [1]. Namely: a || x1 and c* || x3. This differs from the convention chosen by the Fable group, where a* || x1 and c || x3 [2]. *) The unit cell angles are defined as follows: alpha=acos(b'*c/|b||c|), beta=acos(c'*a/|c||a|), and gamma=acos(a'*b/|a||b|). *) The reciprocal lattice vectors are calculated using the crystallographic convention, where the prefactor of 2*pi is omitted. In this convention, the reciprocal lattice volume is 1/V. *) Several relations from [3] were employed in the component calculations. REFERENCES: [1] J. F. Nye, ``Physical Properties of Crystals: Their Representation by Tensors and Matrices''. Oxford University Press, 1985. ISBN 0198511655 [2] E. M. Lauridsen, S. Schmidt, R. M. Suter, and H. F. Poulsen, ``Tracking: a method for structural characterization of grains in powders or polycrystals''. J. Appl. Cryst. (2001). 34, 744--750 [3] R. J. Neustadt, F. W. Cagle, Jr., and J. Waser, ``Vector algebra and the relations between direct and reciprocal lattice quantities''. Acta Cryst. (1968), A24, 247--248 """ # build index for sorting out lattice parameters lattStrings = [ 'cubic' , 'hexagonal' , 'trigonal' , 'rhombohedral', 'tetragonal' , 'orthorhombic', 'monoclinic' , 'triclinic' ] if radians: angConv = 1. else: angConv = pi/180. # degToRad deg90 = pi/2. deg120 = 2.*pi/3. # if tag == lattStrings[0]: # cubic cellparms = num.r_[num.tile(lparms[0], (3,)), deg90*num.ones((3,))] elif tag == lattStrings[1] or tag == lattStrings[2]: # hexagonal | trigonal (hex indices) cellparms = num.r_[lparms[0], lparms[0], lparms[1], deg90, deg90, deg120] elif tag == lattStrings[3]: # rhombohedral cellparms = num.r_[num.tile(lparms[0], (3,)), num.tile(angConv*lparms[1], (3,))] elif tag == lattStrings[4]: # tetragonal cellparms = num.r_[lparms[0], lparms[0], lparms[1], deg90, deg90, deg90] elif tag == lattStrings[5]: # orthorhombic cellparms = num.r_[lparms[0], lparms[1], lparms[2], deg90, deg90, deg90] elif tag == lattStrings[6]: # monoclinic cellparms = num.r_[lparms[0], lparms[1], lparms[2], deg90, angConv*lparms[3], deg90] elif tag == lattStrings[7]: # triclinic cellparms = lparms else: raise RuntimeError('lattice tag \'%s\' is not recognized' % (tag)) if debug: print str(cellparms[0:3]) + ' ' + str(r2d*cellparms[3:6]) alfa = cellparms[3] beta = cellparms[4] gama = cellparms[5] cosalfar, sinalfar = cosineXform(alfa, beta, gama) a = cellparms[0]*num.r_[1, 0, 0] b = cellparms[1]*num.r_[num.cos(gama), num.sin(gama), 0] c = cellparms[2]*num.r_[num.cos(beta), -cosalfar*num.sin(beta), sinalfar*num.sin(beta)] ad = num.sqrt(sum(a**2)) bd = num.sqrt(sum(b**2)) cd = num.sqrt(sum(c**2)) # Cell volume V = num.dot(a, num.cross(b, c)) # F takes components in the direct lattice to X F = num.c_[a, b, c] # Reciprocal lattice vectors astar = num.cross(b, c)/V bstar = num.cross(c, a)/V cstar = num.cross(a, b)/V # and parameters ar = num.sqrt(sum(astar**2)) br = num.sqrt(sum(bstar**2)) cr = num.sqrt(sum(cstar**2)) alfar = num.arccos(num.dot(bstar, cstar)/br/cr) betar = num.arccos(num.dot(cstar, astar)/cr/ar) gamar = num.arccos(num.dot(astar, bstar)/ar/br) # B takes components in the reciprocal lattice to X B = num.c_[astar, bstar, cstar] cosalfar2, sinalfar2 = cosineXform(alfar, betar, gamar) afable = ar*num.r_[1, 0, 0] bfable = br*num.r_[num.cos(gamar), num.sin(gamar), 0] cfable = cr*num.r_[num.cos(betar), -cosalfar2*num.sin(betar), sinalfar2*num.sin(betar)] BR = num.c_[afable, bfable, cfable] U0 = num.dot(B, num.linalg.inv(BR)) if outputDegrees: dparms = num.r_[ad, bd, cd, r2d*num.r_[ alfa, beta, gama]] rparms = num.r_[ar, br, cr, r2d*num.r_[alfar, betar, gamar]] else: dparms = num.r_[ad, bd, cd, num.r_[ alfa, beta, gama]] rparms = num.r_[ar, br, cr, num.r_[alfar, betar, gamar]] L = {'F':F, 'B':B, 'BR':BR, 'U0':U0, 'vol':V, 'dparms':dparms, 'rparms':rparms} return L
def makeScatteringVectors(hkls, rMat_c, bMat, wavelength, chiTilt=None): """ modeled after QFromU.m """ # basis vectors bHat_l = num.c_[ 0., 0., -1.].T eHat_l = num.c_[ 1., 0., 0.].T zTol = 1.0e-7 # zero tolerance for checking vectors gVec_s = [] oangs0 = [] oangs1 = [] # these are the reciprocal lattice vectors in the CRYSTAL FRAME # ** NOTE ** # if strained, assumes that you handed it a bMat calculated from # strained [a, b, c] gVec_c = num.dot( bMat, hkls ) gHat_c = unitVector(gVec_c) dim0, nRefl = gVec_c.shape assert dim0 == 3, "Looks like something is wrong with your lattice plane normals son!" # extract 1/dspacing and sin of bragg angle dSpacingi = columnNorm(gVec_c).flatten() sintht = 0.5 * wavelength * dSpacingi # move reciprocal lattice vectors to sample frame gHat_s = num.dot(rMat_c.squeeze(), gHat_c) if chiTilt is None: cchi = 1. schi = 0. rchi = num.eye(3) else: cchi = num.cos(chiTilt) schi = num.sin(chiTilt) rchi = num.array([[ 1., 0., 0.], [ 0., cchi, -schi], [ 0., schi, cchi]]) pass a = cchi * gHat_s[0, :] b = -cchi * gHat_s[2, :] c = schi * gHat_s[1, :] - sintht # form solution abMag = num.sqrt(a*a + b*b); assert num.all(abMag > 0), "Beam vector specification is infealible!" phaseAng = num.arctan2(b, a) rhs = c / abMag; rhs[abs(rhs) > 1.] = num.nan rhsAng = num.arcsin(rhs) # write ome angle output arrays (NaNs persist here) ome0 = rhsAng - phaseAng ome1 = num.pi - rhsAng - phaseAng goodOnes_s = -num.isnan(ome0) eta0 = num.nan * num.ones_like(ome0) eta1 = num.nan * num.ones_like(ome1) # mark feasible reflections goodOnes = num.tile(goodOnes_s, (1, 2)).flatten() numGood_s = sum(goodOnes_s) numGood = 2 * numGood_s tmp_eta = num.empty(numGood) tmp_gvec = num.tile(gHat_c, (1, 2))[:, goodOnes] allome = num.hstack([ome0, ome1]) for i in range(numGood): come = num.cos(allome[goodOnes][i]) some = num.sin(allome[goodOnes][i]) rome = num.array([[ come, 0., some], [ 0., 1., 0.], [-some, 0., come]]) rMat_s = num.dot(rchi, rome) gVec_l = num.dot(rMat_s, num.dot(rMat_c, tmp_gvec[:, i].reshape(3, 1) ) ) tmp_eta[i] = num.arctan2(gVec_l[1], gVec_l[0]) pass eta0[goodOnes_s] = tmp_eta[:numGood_s] eta1[goodOnes_s] = tmp_eta[numGood_s:] # make assoc tTh array tTh = 2.*num.arcsin(sintht).flatten() tTh0 = tTh; tTh0[-goodOnes_s] = num.nan gVec_s = num.tile(dSpacingi, (3, 1)) * gHat_s oangs0 = num.vstack([tTh0.flatten(), eta0.flatten(), ome0.flatten()]) oangs1 = num.vstack([tTh0.flatten(), eta1.flatten(), ome1.flatten()]) return gVec_s, oangs0, oangs1
def getFriedelPair(tth0, eta0, *ome0, **kwargs): """ Get the diffractometer angular coordinates in degrees for the Friedel pair of a given reflection (min angular distance). AUTHORS: J. V. Bernier -- 10 Nov 2009 USAGE: ome1, eta1 = getFriedelPair(tth0, eta0, *ome0, display=False, units='degrees', convention='hexrd') INPUTS: 1) tth0 is a list (or ndarray) of 1 or n the bragg angles (2theta) for the n reflections (tiled to match eta0 if only 1 is given). 2) eta0 is a list (or ndarray) of 1 or n azimuthal coordinates for the n reflections (tiled to match tth0 if only 1 is given). 3) ome0 is a list (or ndarray) of 1 or n reference oscillation angles for the n reflections (denoted omega in [1]). This argument is optional. 4) Keyword arguments may be one of the following: Keyword Values|{default} Action -------------- -------------- -------------- 'display' True|{False} toggles display info to cmd line 'units' 'radians'|{'degrees'} sets units for input angles 'convention' 'fable'|{'hexrd'} sets conventions defining the angles (see below) 'chiTilt' None the inclination (about Xlab) of the oscillation axis OUTPUTS: 1) ome1 contains the oscialltion angle coordinates of the Friedel pairs associated with the n input reflections, relative to ome0 (i.e. ome1 = <result> + ome0). Output is in DEGREES! 2) eta1 contains the azimuthal coordinates of the Friedel pairs associated with the n input reflections. Output units are controlled via the module variable 'outputDegrees' NOTES: JVB) The ouputs ome1, eta1 are written using the selected convention, but the units are alway degrees. May change this to work with Nathan's global... JVB) In the 'fable' convention [1], {XYZ} form a RHON basis where X is downstream, Z is vertical, and eta is CCW with +Z defining eta = 0. JVB) In the 'hexrd' convention [2], {XYZ} form a RHON basis where Z is upstream, Y is vertical, and eta is CCW with +X defining eta = 0. REFERENCES: [1] E. M. Lauridsen, S. Schmidt, R. M. Suter, and H. F. Poulsen, ``Tracking: a method for structural characterization of grains in powders or polycrystals''. J. Appl. Cryst. (2001). 34, 744--750 [2] J. V. Bernier, M. P. Miller, J. -S. Park, and U. Lienert, ``Quantitative Stress Analysis of Recrystallized OFHC Cu Subject to Deformed In Situ'', J. Eng. Mater. Technol. (2008). 130. DOI:10.1115/1.2870234 """ dispFlag = False fableFlag = False chi = None c1 = 1. c2 = pi/180. zTol = 1.e-7 # cast to arrays (in case they aren't) if num.isscalar(eta0): eta0 = [eta0] if num.isscalar(tth0): tth0 = [tth0] if num.isscalar(ome0): ome0 = [ome0] eta0 = num.asarray(eta0) tth0 = num.asarray(tth0) ome0 = num.asarray(ome0) if eta0.ndim != 1: raise RuntimeError, 'your azimuthal input was not 1-D, so I do not know what you expect me to do' npts = len(eta0) if tth0.ndim != 1: raise RuntimeError, 'your Bragg angle input was not 1-D, so I do not know what you expect me to do' else: if len(tth0) != npts: if len(tth0) == 1: tth0 = tth0*num.ones(npts) elif npts == 1: npts = len(tth0) eta0 = eta0*num.ones(npts) else: raise RuntimeError, 'the azimuthal and Bragg angle inputs are inconsistent' if len(ome0) == 0: ome0 = num.zeros(npts) # dummy ome0 elif len(ome0) == 1 and npts > 1: ome0 = ome0*num.ones(npts) else: if len(ome0) != npts: raise RuntimeError('your oscialltion angle input is inconsistent; ' \ + 'it has length %d while it should be %d' % (len(ome0), npts) ) # keyword args processing kwarglen = len(kwargs) if kwarglen > 0: argkeys = kwargs.keys() for i in range(kwarglen): if argkeys[i] == 'display': dispFlag = kwargs[argkeys[i]] elif argkeys[i] == 'convention': if kwargs[argkeys[i]].lower() == 'fable': fableFlag = True elif argkeys[i] == 'units': if kwargs[argkeys[i]] == 'radians': c1 = 180./pi c2 = 1. elif argkeys[i] == 'chiTilt': if kwargs[argkeys[i]] is not None: chi = kwargs[argkeys[i]] # a little talkback... if dispFlag: if fableFlag: print '\nUsing Fable angle convention\n' else: print '\nUsing image-based angle convention\n' # mapped eta input # - in DEGREES, thanks to c1 eta0 = mapAngle(c1*eta0, [-180, 180], units='degrees') if fableFlag: eta0 = 90 - eta0 # must put args into RADIANS # - eta0 is in DEGREES, # - the others are in whatever was entered, hence c2 eta0 = d2r*eta0 tht0 = c2*tth0/2 if chi is not None: chi = c2*chi else: chi = 0 # --------------------- # SYSTEM SOLVE # # # cos(chi)cos(eta)cos(theta)sin(x) - cos(chi)sin(theta)cos(x) = sin(theta) - sin(chi)sin(eta)cos(theta) # # # Identity: a sin x + b cos x = sqrt(a**2 + b**2) sin (x + alfa) # # / # | atan(b/a) for a > 0 # alfa < # | pi + atan(b/a) for a < 0 # \ # # => sin (x + alfa) = c / sqrt(a**2 + b**2) # # must use both branches for sin(x) = n: x = u (+ 2k*pi) | x = pi - u (+ 2k*pi) # cchi = num.cos(chi); schi = num.sin(chi) ceta = num.cos(eta0); seta = num.sin(eta0) ctht = num.cos(tht0); stht = num.sin(tht0) nchi = num.c_[0., cchi, schi].T gHat0_l = num.vstack([ceta * ctht, seta * ctht, stht]) a = cchi*ceta*ctht b = cchi*schi*seta*ctht + schi*schi*stht - stht c = stht + cchi*schi*seta*ctht + schi*schi*stht # form solution abMag = num.sqrt(a*a + b*b); assert num.all(abMag > 0), "Beam vector specification is infealible!" phaseAng = num.arctan2(b, a) rhs = c / abMag; rhs[abs(rhs) > 1.] = num.nan rhsAng = num.arcsin(rhs) # write ome angle output arrays (NaNs persist here) ome1 = rhsAng - phaseAng ome2 = num.pi - rhsAng - phaseAng ome1 = mapAngle(ome1, [-num.pi, num.pi], units='radians') ome2 = mapAngle(ome2, [-num.pi, num.pi], units='radians') ome_stack = num.vstack([ome1, ome2]) min_idx = num.argmin(abs(ome_stack), axis=0) ome_min = ome_stack[min_idx, range(len(ome1))] eta_min = num.nan * num.ones_like(ome_min) # mark feasible reflections goodOnes = -num.isnan(ome_min) numGood = sum(goodOnes) tmp_eta = num.empty(numGood) tmp_gvec = gHat0_l[:, goodOnes] for i in range(numGood): come = num.cos(ome_min[goodOnes][i]) some = num.sin(ome_min[goodOnes][i]) rchi = rotMatOfExpMap( num.tile(ome_min[goodOnes][i], (3, 1)) * nchi ) gHat_l = num.dot(rchi, tmp_gvec[:, i].reshape(3, 1)) tmp_eta[i] = num.arctan2(gHat_l[1], gHat_l[0]) pass eta_min[goodOnes] = tmp_eta # everybody back to DEGREES! # - ome1 is in RADIANS here # - convert and put into [-180, 180] ome1 = mapAngle( mapAngle(r2d*ome_min, [-180, 180], units='degrees') + c1*ome0, [-180, 180], units='degrees') # put eta1 in [-180, 180] eta1 = mapAngle(r2d*eta_min, [-180, 180], units='degrees') if not outputDegrees: ome1 = d2r * ome1 eta1 = d2r * eta1 return ome1, eta1