Source code for sloth.inst.rowland

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Rowland circle geometry
==========================

Units here:

 angular [deg]
 spatial [mm]
 energy [eV]

Table of variables and conventions

| description            | here    | XRT    | RT4XES  | SHADOW3 |
|------------------------+---------+--------+---------+---------|
| Bragg angle            | \theta  | \theta | \theta  | \theta  |
| Rowland circle radius  | Rm      | Rm     | R/2     |         |
| crystal bending radius | 2*Rm    | 2*Rm   | R       |         |
| rays direction         | Y       | Y      | X       | Y       |
| sagittal direction     | X       | X      | Y       | X       |
| meridional direction   | Z       | Z      | Z       |         |
| sample pos (X,Y,Z)     | (0,0,0) |        | (0,0,0) |         |
|                        |         |        |         |         |

.. note::

   The sagittal plane local reference system is a 2D coordinate
   system. All analysers are sitting/sliding on this plane. The origin
   is located at the central analyser axis at "aL" parametric distance
   from the central analyser surface (away from the sample). The
   coordinates are (aXoff, SagOff). ``aXoff`` is positive on the right
   of the central analyser when looking at the sample. ``SagOff`` is
   positive toward the sample.

.. note::

   The following code has been tested with a 3D CAD model built using
   SolidWorks: ``RowlandSketchPrototype-v1512``

.. note ::

   Lens equations used here are taken from:

   - Suortti et al., J. Synchrotron Rad. 6, 69 (1999), DOI: 10.1107/S0909049599000291
   - Podorov et al., J. Phys. D: Appl. Phys 34, 2363 (2001), DOI: 10.1088/0022-3727/34/15/317

   *Meridional*

   p0 = 2 * Rm * sin(theta0 - alpha)
   q0 = 2 * Rm * sin(theta0 + alpha)

   p0/p + q0/q = 2

   *Sagittal*

   1/p + 1/q = ( sin(theta0 - alpha) + sin(theta0 + alpha) ) / Rs

RT4XES
------

ID26specVII.m

- sample at (0,0,0); crystal at (Xa,0,Za); detector at (0,0,Zd=2*Za)

TODO
----

- all the bender-related things should go only in RcHoriz!!!
- check the full thing formulas when a miscut is given (alpha != 0)

"""

__author__ = "Mauro Rovezzi"
__email__ = "mauro.rovezzi@gmail.com"
__license__ = "BSD license <http://opensource.org/licenses/BSD-3-Clause>"

import sys, os, math
import numpy as np

from ..math.rotmatrix import rotate

try:
    xrange
except NameError:
    xrange = range

DEBUG = 0

### GLOBAL VARIABLES ###
HC = 1.2398418743309972e-06 # eV * m
ED0 = 1e-4 # minimum energy step (eV) considered as 0 
AZ0 = 1e-4 # minimum Z step (mm) considered as 0

### UTILITIES ###
[docs] def cs_h(c, R): """Height of the circular segment, given its radius R and chord length c See: [http://en.wikipedia.org/wiki/Circular_segment] """ if c >= 2*R: print('WARNING: the chord is greater than the diameter!') print('WARNING: returning maximum height, the radius') return R else: return R - math.sqrt( R**2 - (c**2/4) )
[docs] def acenx(n, asx=25., agx=5.): """n-th analyser center (starting from 0!) in x, given its size (asx) and gap between two (agx) in mm """ return (asx + agx) * n
[docs] def det_pos_rotated(dxyz, drot=35., doffsets=[0,0]): """return the detector positions in a rotated reference system with the origin at the sample Parameters ---------- dxyz : numpy array of floats [X, Y, Z] detector position in the global coordinate system (detector assumed on the YZ plane) drot : float [35.] angle of rotation, counter-clock-wise, around X axis doffsets : numpy array of floats [0, 0] [Y, Z] offsets of the detector reference system origin Return ------ [dy, dz] : numpy array of floats absolute positions of the detector stages NOTE: the (dy, dz) sign here is given as the convention setted during the commissioning in Aug 2017 """ x, y, z = dxyz[0], dxyz[1], dxyz[2] dr = math.sqrt(y**2 + z**2) if y == 0.: alpha = math.pi/2. - math.radians(drot) else: alpha = math.atan(z/y) - math.radians(drot) if DEBUG: print('DEBUG(det_pos_rotated): alpha is {0} deg'.format(math.degrees(alpha))) dpar = dr * math.cos(alpha) dper = dr * math.sin(alpha) #insert offset of the detector origin dy = dpar - doffsets[0] dz = -(doffsets[1] - dper) return np.array([dy, dz])
### CLASS ###
[docs] class RowlandCircle(object): """Rowland circle geometry""" def __init__(self, Rm=500., theta0=0., alpha=0., d=None,\ aW=0., aWext=0., rSext=0., aL=0.,\ bender_version=None, bender=(0., 0., 0.), actuator=(0., 0.),\ inCircle=False, useCm=False, showInfos=True, **kws): """ Parameters ---------- Rm : float, 500. radius of the Rowland circle (meridional radius) in [mm] theta0 : float, 0. Bragg angle for the central Rowland circle [deg] alpha : float, 0. miscut angle in deg: the angle between the surface of the crystal and the crystal planes at the centre point; for positive alpha, q > p, that is, the center analyser moves downward (or clockwise) the vertical Rowland circle d : float, None crystal d-spacing in \AA (this is simply an utility to convert theta to energy - in eV) aW : float, 0. crystal analyser optical width aWext : float, 0. crystal analyser extended width (NOTE: this width is used in self.get_chi2, that is, the width to get two adjacent analysers touching) rSext : float, 0. sagittal radius offset where aWext is referred to, that is, aWext condition is given for Rs+rSext aL : float, 0. distance of analyser center from the chi rotation (affects => Chi, SagOff) bender_version : string, None defines the bender tuple keyword argument see -> self.get_bender_pos() bender : tuple of floats, (0., 0., 0.) corresponds to if (bender_version is None) or (bender_version == 0): (length_arm0_mm, length_arm1_mm, angle_between_arms_deg) if (bender_version == 1): (length_arm0_mm, length_arm1_mm, length_anchor_actuator_mm) actuator : tuple of floats, (0., 0.) corresponts to (axoff_actuator_mm, length_actuator_arm_mm) inCircle : boolean, False sample on the Rowland cicle otherwise give the y offset if inside (dispersive) useCm : boolean, False use cm instead of mm (as default in SHADOW) showInfos : boolean, True print extra informations (sometimes useful) Returns ------- None (set attributes) self.Rm self.inCircle self.sampPos self.alpha self.ralpha self.aL self.aW self.aWext self.rSext self.bender self.actuator self.bender_version self.showInfos """ if useCm: self.uDist = 'cm' else: self.uDist = 'mm' self.Rm = Rm self.aW = aW self.aWext = aWext self.rSext = rSext self.bender = bender self.actuator = actuator if bender_version is None: bender_version = 0 self.bender_version = bender_version self.aL = aL self.Rs = 0. self.d = d self.inCircle = inCircle self.showInfos = showInfos self.infos_dict = {} if inCircle is False: self.sampPos = np.array([0,0,0]) elif (('int' in str(type(inCircle))) or ('float' in str(type(inCircle)))): self.sampPos = np.array([0,inCircle,0]) print('WARNING: inCircle not tested/implementd yet, REVERTED to 0 position!') self.sampPos = np.array([0,0,0]) else: raise NameError('Sample inside the Rowland circle: y offset is required') self.alpha = alpha self.ralpha = np.deg2rad(self.alpha) if (theta0 != 0) : self.set_theta0(theta0) #no need to return self; this method will not be called in sequence #return self
[docs] def set_dspacing(self, d): """set crystal d-spacing (\AA)""" self.d = d
[docs] def set_theta0(self, theta0, showInfos=None): """set correct attributes for a given theta0 (Bragg angle = center theta) Returns ------- None (set attributes) self.theta0 : in degrees self.rtheta0 : in radians self.sd : sample-detector distance (independent of \alpha?) self.p : sample-analyser distance self.q : analyser-detector distance self.Rs : sagittal radius (analyser center, self.aL == 0.) """ if showInfos is None: showInfos = self.showInfos self.theta0 = theta0 self.rtheta0 = math.radians(self.theta0) self.sd = 2. * self.Rm * math.sin(2. * self.rtheta0) self.p0 = 2. * self.Rm * math.sin(self.rtheta0 - self.ralpha) #self.p = self.p0 - self.sampPos[1] #not fully tested yet self.p = self.p0 self.q0 = 2. * self.Rm * math.sin(self.rtheta0 + self.ralpha) self.q = self.q0 # TODO: generic case! if self.p == self.p0: if self.alpha == 0: if self.showInfos: print('INFO: sagittal focusing, symmetric formula') self.Rs = 2 * self.Rm * (math.sin(self.rtheta0))**2 # no miscut else : print('WARNING: sagittal focusing with miscut (CHECK FORMULA!)') #self.Rs = self.Rm * (math.cos(2*self.ralpha) - math.cos(2*self.theta0)) # TODO: check this self.Rs = 2 * self.Rm * math.sin(self.rtheta0 - self.ralpha) * math.sin(self.rtheta0 + self.ralpha) #this one should be correct: TO TEST! else : # generic sagittal focusing # TODO: check this!!! print('WARNING: sagittal focusing generic (CHECK FORMULA!)') self.Rs = ( 2. * math.sin(self.rtheta0) * self.p * self.q ) / (self.p + self.q) if showInfos: print("INFO: theta0 = {0:.3f} deg".format(self.theta0)) print("INFO: alpha = {0:.3f} deg".format(self.alpha)) if self.d is not None: print("INFO: ene0 = {0:.2f} eV".format(self.get_ene())) print("INFO: d = {0:.3f} \AA".format(self.d)) print("INFO: p = {0:.3f} {1}".format(self.p, self.uDist)) print("INFO: q = {0:.3f} {1}".format(self.q, self.uDist)) print("INFO: Rm = {0:.3f} {1}".format(self.Rm, self.uDist)) print("INFO: Rs = {0:.3f} {1}".format(self.Rs, self.uDist)) print("INFO: aW = {0:.3f} {1}".format(self.aW, self.uDist)) print("INFO: aWext = {0:.3f} {1}".format(self.aWext, self.uDist)) print("INFO: aL = {0:.3f} {1}".format(self.aL, self.uDist)) return self
[docs] def get_infos(self): """show useful information on distances/parameters""" self.infos_dict.update({'theta0' : [self.theta0, 'deg'], 'p' : [self.p, self.uDist], 'q' : [self.q, self.uDist], 'Rm' : [self.Rm, self.uDist], 'Rs' : [self.Rs, self.uDist], 'aW' : [self.aW, self.uDist], 'aWext' : [self.aWext, self.uDist], 'aL' : [self.aL, self.uDist]}) if self.d is not None: self.infos_dict.update({'d' : [self.d, '\AA'], 'ene0' : [self.get_ene(), 'eV']}) return self.infos_dict
[docs] def set_ene0(self, ene0, d=None): """set the central energy (eV) and relative Bragg angle""" if d is None: d = self.d try: theta0 = self.get_theta(ene0, d=d, isDeg=True) self.set_theta0(theta0) except: print("ERROR: energy not setted!")
[docs] def get_theta(self, ene=None, d=None, isDeg=True): """get theta angle (deg or rad, controlled by isDeg var) for a given energy (eV) and d-spacing""" if d is None: d = self.d if ene is None: ene = self.get_ene(theta=None, d=d, isDeg=isDeg) if (d is not None) and not (self.d == 0) and not (ene == 0): wlen = ( HC / ene ) * 1e10 theta = math.asin( wlen / (2*d) ) if isDeg: theta = math.degrees(theta) return theta else: raise NameError("wrong d-spacing or energy")
[docs] def get_ene(self, theta=None, d=None, isDeg=True): """get energy (eV) for a given angle (deg) and d-spacing""" if theta is None: theta = self.rtheta0 isDeg = False if d is None: d = self.d if isDeg: rtheta = math.radians(theta) else: rtheta = theta if d is not None: wlen = 2 * d * math.sin(rtheta) return ( HC / wlen ) * 1e10 else: raise NameError("give d-spacing (\AA)")
[docs] def get_dth(self, eDelta): """Delta\theta using differential Bragg law""" if abs(eDelta) <= ED0: return 0 ene = self.get_ene(theta=self.rtheta0, isDeg=False) return -1 * ( eDelta / ene ) * math.tan(self.rtheta0)
[docs] def get_chi(self, aXoff, Rs=None, aL=None, inDeg=True): """get \chi angle in sagittal focusing using offset from centre analyser (aXoff)""" if Rs is None: Rs = self.Rs if aL is None: aL = self.aL Rs2 = Rs + aL rchi = math.atan( aXoff / math.sqrt(Rs2**2 - aXoff**2) ) if (inDeg is True): return np.rad2deg(rchi) else: return rchi
[docs] def get_chi2(self, aN=1., aWext=None, Rs=None, rSext=None, inDeg=True): """get \chi angle in sagittal focusing using touching/connected analysers Description ----------- Using the extended radius Rsp=Rs+rSext, can be calculated as aN times the chi/2 of the first analyser given by: \chi/2 = \atan((aWext/2)/Rsp) Parameters ---------- aN : float, 1. n-th analyser (0 is the central one) """ if aWext is None: aWext = self.aWext if Rs is None: Rs = self.Rs if rSext is None: rSext = self.rSext Rsp = Rs + rSext rchi = ( 2 * math.atan((aWext/2)/Rsp) ) * aN if (inDeg is True): return math.degrees(rchi) else: return rchi
[docs] def get_ana_dist(self, chi, aN=1., aW=None, Rs=None, inDeg=True): """get analyser-analyser distance at Rs""" if aW is None: aW = self.aW if Rs is None: Rs = self.Rs if not (aN == 0): chi = chi/aN if inDeg: chihalf = math.radians(chi/2.) else: chihalf = chi/2. aDist = 2 * Rs * math.sin(chihalf) - aW * math.cos(chihalf) if self.showInfos: print('INFO: analyser #{0:.0f}-#{1:.0f} (edge-to-edge) = {2:.4f} {3}'.format(aN, aN-1, aDist, self.uDist)) print('INFO: delta chi = {0:.4f} deg'.format(chi)) return aDist
[docs] def get_axoff(self, chi, Rs=None, aL=None): """get aXoff for the pivot point when chi is known (simple case)""" if Rs is None: Rs = self.Rs if aL is None: aL = self.aL return (Rs + aL) * math.sin(math.radians(chi))
[docs] def get_axoff0(self, chi, Rs=None): """get aXoff at the surface of the analyser""" return self.get_axoff(chi, Rs=Rs, aL=0)
[docs] def get_axoff_line(self, aXoffMin, SagOffMin, degRot=0., Rs=None, aL=None): """get aXoff for the pivot point when only a linear trajectory is known Description ----------- The local sagittal cartesian coordinate system is assumed: the origin is at the pivot point of the central analyser, the abscissa is pointing toward the center of the sagittal circle and the ordinate is pointing on the right side when looking in the abscissa direction. In the following is given the solution of the linear equations system consisting in the interception of the sagittal circle with the linear trajectory of the pivot point, that is: (x-Rs-aL)^2 + y^2 - (Rs+aL)^2 = 0 x = x0 - d*cos(phi) y = y0 + d*sin(phi) where: x is SagOff y is aXoff x0, y0 is SagOffMin, aXoffMin at minimum Rs phi is degRot d is the distance of x, y from x0, y0 on the local polar coordinate system of the pivot point Parameters ---------- aXoffMin, SagOffMin : float coordinates of the minimum position of the pivot point on the linear trajectory on the sagittal plane. degRot : float, 0. angle (deg) of the linear trajectory with respect to the abscissa (horizontal) Returns ------- aXoff1 : float """ if Rs is None: Rs = self.Rs if aL is None: aL = self.aL y0 = aXoffMin x0 = SagOffMin phi = math.radians(degRot) if (phi == 0): if self.showInfos: print('INFO: simple case where aXoff is constant at aXoffMin') print('INFO: aXoffMin = {0:.5f}'.format(aXoffMin)) return aXoffMin sinphi = math.sin(phi) cosphi = math.cos(phi) a = 1 #sinphi**2 + cosphi**2 b = -2*x0*cosphi + 2*Rs*cosphi + 2*y0*sinphi + 2*aL*cosphi c = x0**2 + y0**2 - 2*Rs*x0 - 2*aL*x0 #solutions to: a*y**2 + b*y + c = 0 y1 = (-b + math.sqrt(b**2 - 4*a*c)) / (2*a) #good solution! y2 = (-b - math.sqrt(b**2 - 4*a*c)) / (2*a) if self.showInfos: print('INFO: two solutions for polar distance d:') print('INFO: 1 = {0:.5f} (good)'.format(y1)) print('INFO: 2 = {0:.5f} (bad)'.format(y2)) aXoff1 = y1*sinphi + aXoffMin SagOff1 = SagOffMin - y1*cosphi aXoff2 = y2*sinphi + aXoffMin SagOff2 = SagOffMin - y2*cosphi if self.showInfos: print('INFO: aXoffMin = {0:.5f}, SagOffMin = {1:.5f}, degRot = {2:.3f}'.format(aXoffMin, SagOffMin, degRot)) print('INFO: aXoff1 = {0:.5f}, SagOff1 = {1:.5f}'.format(aXoff1, SagOff1)) print('INFO: aXoff2 = {0:.5f}, SagOff2 = {1:.5f}'.format(aXoff2, SagOff2)) return aXoff1
[docs] def get_sag_off(self, aXoff, Rs=None, aL=None, retAll=False): """analyser sagittal offset from the center one (Y-like direction) Parameters ---------- aXoff : float, required position at the pivot point (will give a wrong result using the position at the analyser surface) retAll : boolean, False control return Returns ------- if retAll: list of float [chi_angle_deg, aXoff, SagOff, chi0_angle_deg, aXoff0, SagOff0] where the 0 is referred to the case with aL=0 else: float SagOff """ if Rs is None: Rs = self.Rs if aL is None: aL = self.aL rchi = self.get_chi(aXoff, Rs=Rs, aL=aL, inDeg=False) aXoff0 = aXoff - aL*math.sin(rchi) rchi0 = self.get_chi(aXoff0, Rs=Rs, aL=0, inDeg=False) #to check this is equal to rchi! SagOff0 = cs_h(aXoff0*2, Rs) SagOff = SagOff0 - aL*math.cos(rchi) + aL if self.showInfos: print("INFO: === surface (0) vs pivot (aL={0:.0f}) ===".format(aL)) _tmpl_ihead = "INFO: {0:=^10} {1:=^12} {2:=^13}" _tmpl_idata = "INFO: {0:^ 10.5f} {1:^ 12.5f} {2:^ 13.5f}" print(_tmpl_ihead.format('Chi', 'aXoff', 'SagOff')) print(_tmpl_idata.format(math.degrees(rchi), aXoff, SagOff)) print(_tmpl_ihead.format('Chi0', 'aXoff0', 'SagOff0')) print(_tmpl_idata.format(math.degrees(rchi0), aXoff0, SagOff0)) if retAll: return [math.degrees(rchi), aXoff, SagOff, math.degrees(rchi0), aXoff0, SagOff0] else: return SagOff
[docs] def get_sag_off0(self, aXoff): """quick wrap to get the sagittal offset at the analyser surface, use get_sag_off() for full control!""" return self.get_sag_off(aXoff, retAll=True)[-1]
[docs] def get_sag_off_mots(self, aXoff, degRot=0., pivotSide=10., Rs=None, aL=None): """motors positions for sagittal offset TODO: not working yet, sagoff also negative """ print('WARNING: deprecated/broken method!!!') return 0, 0
# sagoffs = self.get_sag_off(aXoff, Rs=Rs, aL=aL, retAll=True) # tS = sagoffs[2] / math.cos(math.radians(degRot)) # rS = sagoffs[0] - degRot # tPS = pivotSide * math.sin(math.radians(rS)) # if self.showInfos: # print('Pivot center : {0}'.format(tS)) # print('Pivot side ({0}): +/- {1}'.format(pivotSide, tPS)) # return tS + tPS, tS - tPS
[docs] def get_bender_pos(self, aN=5, bender=None, Rs=None, aL=None, rSext=None, bender_version=None): """get the position (aXoff, SagOff) of the bender point (B)""" if aN < 3: print('ERROR: this method works only for aN>=3') return (0., 0.) if bender is None: bender = self.bender if Rs is None: Rs = self.Rs if aL is None: aL = self.aL if rSext is None: rSext = self.rSext if bender_version is None: bender_version = self.bender_version #map last 3 pivot points positions _c2 = [self.get_chi2(_n) for _n in xrange( int(aN-2), int(aN+1) )] #CHIs dchi = _c2[2]-_c2[0] if self.showInfos: print('INFO: == CHI ==') print('INFO: \chi{0:.0f} = {1:.5f}'.format(aN, _c2[2])) print('INFO: \Delta\chi{0}{1} = {2:.5f} deg'.format(aN, aN-2, dchi)) _p = [self.get_sag_off(self.get_axoff(_cn), retAll=True) for _cn in _c2] #SagOffs #find the angle between the last pivot point _p[-1] and the bender point (B) #we use for this the position of the end point of bender[1] (C) _R = Rs + aL rdch = math.radians(dchi/2.) h = _R * (1 - math.cos(rdch)) #chord between pivots 0 and -2 chalf = _R * math.sin(rdch) #half the chord length from circular segment formula #find coordinates of point B (pb) of the bender (anchor point with actuator[1]) ra = math.acos(chalf/bender[1]) dc = bender[1] * math.sin(ra) - h #aperture of the pantograph along radius if bender_version == 0: sc = self.get_axoff(_c2[1], Rs=Rs+dc) #aXoff_point_C #pc = self.get_sag_off(sc, retAll=True) axlp = _p[2][1] #aXoff last pivot point rb = math.acos( (axlp-sc) / bender[1]) rc = math.pi - math.radians(bender[2]) - rb pb_axoff = axlp + bender[0] * math.cos(rc) #aXoff_bender_point(B) pb_sagoff = axlp - bender[0] * math.sin(rc) #SagOff_bender_point(B) elif bender_version == 1: adc = math.asin( (dc/2) / bender[0] ) #angle opposite to dc pdc = bender[0] * math.cos(adc) #perpendicular to dc pb_ang = math.atan(pdc / (Rs + aL + dc/2)) #angle between last analyzer and point B pb_h = _R * (1 - math.cos(pb_ang)) #chord for the anchoring point pb_chalf = _R * math.sin(pb_ang) #from circular segment formula pb_ra = math.acos(pb_chalf / bender[0]) pb_dc = bender[0] * math.sin(pb_ra) - pb_h pb_chi = pb_ang + self.get_chi2(aN, inDeg=False) #radians pb_rs = Rs + aL + pb_dc pb_axoff = pb_rs * math.sin(pb_chi) pb_sagoff = _R - (pb_rs * math.cos(pb_chi)) print(pb_axoff, pb_sagoff) else: raise NameError("ERROR with bender_version") if self.showInfos: print('INFO: bender point (B) coordinates (local sagittal reference)') print('INFO: aXoff={0:.5f}, SagOff={1:.5f}'.format(pb_axoff, pb_sagoff)) return (pb_axoff, pb_sagoff)
[docs] def get_bender_mot(self, bender_pos, actuator=None, bender_version=None): """get the motor position of the bender, given its point position (B) and the actuator specifications, plus the version of the mechanics Parameters ---------- bender_pos : tuple of floats (bender_axoff_mm, bender_sagoff_mm) actuator : tuple of floats, None (axoff_actuator_mm, length_actuator_mm) bender_version : version of the mechanics 0 -> prototype 1 -> pantograph 2017 """ if actuator is None: actuator = self.actuator if bender_version is None: bender_version = self.bender_version try: if bender_version == 0: rd = math.asin( (actuator[0] - bender_pos[0]) / actuator[1] ) elif bender_version == 1: rd = math.asin((actuator[0] - self.bender[2] - bender_pos[0]) / actuator[1]) else: raise NameError("ERROR with bender_version") mot_sagoff = actuator[1] * math.cos(rd) + bender_pos[1] return mot_sagoff except: print('ERROR with bender actuator position') return 0.
[docs] def get_az_off(self, eDelta, rtheta0=None, d=None, Rm=None): """get analyser Z offset for a given energy delta (eV)""" if abs(eDelta) <= ED0: return 0. if rtheta0 is None: rtheta0 = self.rtheta0 if d is None: d = self.d if d is None: raise NameError("give d-spacing") if Rm is None: Rm = self.Rm _dth = self.get_dth(eDelta) if self.showInfos: print('INFO: dth = {0:.1f} urad ({1:.5f} deg)'.format(_dth*1e6, math.degrees(_dth))) print('INFO: daz [tan(dth) ~ dth] = {0}'.format(_dth * 2 * Rm * math.sin(rtheta0) )) print('INFO: daz [tan(dth) ~ dth and sin(th) ~ 1 = {0}'.format(_dth * 2 * Rm) ) return 2 * Rm * math.sin(rtheta0) * math.tan(_dth)
[docs] def get_ay_off(self, eDelta, rtheta0=None, d=None, Rm=None): """get analyser Y offset for a given energy delta (eV)""" if abs(eDelta) <= ED0: return 0. if rtheta0 is None: rtheta0 = self.rtheta0 if d is None: d = self.d if d is None: raise NameError("give d-spacing") if Rm is None: Rm = self.Rm _dth = self.get_dth(eDelta) if self.showInfos: print('INFO: dth = {0:.1f} urad ({1:.5f} deg)'.format(_dth*1e6, math.degrees(_dth))) return 2 * Rm * math.tan(rtheta0) * math.tan(_dth)
[docs] def get_ene_off(self, aZoff, rtheta0=None, d=None, Rm=None): """get analyser delta E for a given Z offset """ if abs(aZoff) <= AZ0: return 0. if rtheta0 is None: rtheta0 = self.rtheta0 if d is None: d = self.d if d is None: raise NameError("give d-spacing") if Rm is None: Rm = self.Rm # _dth = math.atan( aZoff / (2 * Rm * math.sin(rtheta0)) ) if self.showInfos: print('INFO: dth = {0:.1f} urad ({1:.5f} deg)'.format(_dth*1e6, math.degrees(_dth))) _ene = self.get_ene(theta=rtheta0, d=d, isDeg=False) _de = _ene * _dth / math.tan(rtheta0) return _de
[docs] class RcVert(RowlandCircle): """Rowland circle vertical frame: sample-detector on XZ plane along Z axis """ def __init__(self, *args, **kws): """RowlandCircle init Parameters ========== rotHor : boolean, rotate to horizontal [False] """ try: self.rotHor = kws.pop('rotHor') except: self.rotHor = False RowlandCircle.__init__(self, *args, **kws)
[docs] def get_pos(self, vect): """utility method: return 'vect' or its rotated form if self.rotHor""" if self.rotHor: return rotate(vect, np.array([1,0,0]), (math.pi/2.-self.rtheta0)) else: return vect
[docs] def get_det_pos(self): """detector center position [X,Y,Z]""" zDet = 4 * self.Rm * math.sin(self.rtheta0) * math.cos(self.rtheta0) vDet = np.array([0, 0, zDet]) return self.get_pos(vDet)
[docs] def get_ana_pos(self, chi=0.): """analyser XYZ center position for a given chi Parameters ========== chi : float, 0. [deg] rotation angle on the sagittal plane (hor plane here) """ yAcen = 2 * self.Rm * math.sin(self.rtheta0)**2 zAcen = 2 * self.Rm * math.sin(self.rtheta0) * math.cos(self.rtheta0) Acen = np.array([0, yAcen, zAcen]) if (chi == 0.): return self.get_pos(Acen) else: Aside = rotate(Acen, np.array([0,0,1]), math.radians(chi)) return self.get_pos(Aside)
[docs] def get_miscut_off(self, alpha=None, Rm=None): """returns horizontal and vertical offsets for a given miscut angle TODO: NOT CORRECT, CHECK THIS! """ if (alpha is None) or (Rm is None): ralpha = self.ralpha Rm = self.Rm return - Rm * (1-math.cos(ralpha)), - Rm * math.sin(ralpha)
[docs] class RcHoriz(RowlandCircle): """Rowland circle horizontal frame: sample-analyser on XY plane along Y axis """ def __init__(self, *args, **kws): """RowlandCircle init """ RowlandCircle.__init__(self, *args, **kws)
[docs] def get_det_pos(self): """detector position [X,Y,Z]""" yDet = self.p + self.q * math.cos(2 * self.rtheta0) zDet = self.q * math.sin(2 * self.rtheta0) return np.array([0, yDet, zDet])
[docs] def get_ana_pos(self, chi=0.): """analyser XYZ center position for a given chi Parameters ========== chi : float, 0. [deg] rotation angle on the sagittal plane (around sample-detector axis) """ Acen = np.array([0, self.q, 0]) if (chi == 0.): return Acen else: SDax = self.get_det_pos() - self.sampPos Aside = rotate(Acen, SDax, math.radians(chi)) return Aside
[docs] def get_miscut_off(self, alpha=None, p=None): """returns horizontal and vertical offsets for a given miscut angle TODO: NOT CORRECT CHECK THIS! """ if (alpha is None) or (p is None): ralpha = self.ralpha p = self.p return - p * math.cos(ralpha/2.), - p * math.sin(ralpha/2.)
if __name__ == "__main__": #tests/examples in rowland_tests.py pass