# This file is part of xrayutilities.
#
# xrayutilities is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2009-2010 Eugen Wintersberger <eugen.wintersberger@desy.de>
# Copyright (c) 2009-2023 Dominik Kriegner <dominik.kriegner@gmail.com>
# Copyright (C) 2012 Tanja Etzelstorfer <tanja.etzelstorfer@jku.at>
"""
module helping with planning and analyzing experiments.
various classes are provided for describing experimental geometries,
calculationof angular coordinates of Bragg reflections, conversion of angular
coordinates to Q-space and determination of powder diffraction peak positions.
The strength of the module is the versatile QConversion module which can be
configured to describe almost any goniometer geometry.
"""
import copy
import enum
import numbers
import re
import warnings
import numpy
from numpy.linalg import norm
# package internal imports
from . import config, cxrayutilities, math, utilities
from .exception import InputError, UsageError
# regular expression to check goniometer circle syntax
directionSyntax = re.compile("[xyz][+-]")
circleSyntaxDetector = re.compile("([xyz][+-])|(t[xyz])")
circleSyntaxSample = re.compile("[xyzk][+-]")
[docs]
class QConvFlags(enum.IntFlag):
NONE = 0
HAS_TRANSLATIONS = 1
HAS_SAMPLEDIS = 4
VERBOSE = 16
[docs]
class QConversion:
"""
Class for the conversion of angular coordinates to momentum space for
arbitrary goniometer geometries and X-ray energy. Both angular scans
(where some goniometer angles change during data acquisition) and energy
scans (where the energy is varied during acquisition) as well as mixed
cases can be treated.
the class is configured with the initialization and does provide three
distinct routines for conversion to momentum space for
* point detector: point(...) or __call__()
* linear detector: linear(...)
* area detector: area(...)
linear() and area() can only be used after the init_linear()
or init_area() routines were called
"""
_valid_init_kwargs = {'en': 'x-ray energy',
'wl': 'x-ray wavelength',
'UB': 'orientation/orthonormalization matrix'}
_valid_call_kwargs = {'delta': 'angle offsets',
'wl': 'x-ray wavelength',
'en': 'x-ray energy',
'UB': 'orientation/orthonormalization matrix',
'deg': 'True if angles are in degrees',
'sampledis': 'sample displacement vector'}
_valid_linear_kwargs = {'Nav': 'number of channels for block-average',
'roi': 'region of interest'}
[docs]
def __init__(self, sampleAxis, detectorAxis, r_i, **kwargs):
"""
initialize QConversion object.
This means the rotation axis of the sample and detector circles
need to be given: starting with the outer most circle.
Parameters
----------
sampleAxis : list or tuple
sample circles, e.g. ['x+', 'z+'] would mean two sample circles
whereas the outer one turns righthanded around the x axis and the
inner one turns righthanded around z.
detectorAxis : list or tuple
detector circles e.g. ['x-'] would mean a detector arm with a
single motor turning lefthanded around the x-axis.
r_i : array-like
vector giving the direction of the primary beam (length is relevant
only if translations are involved)
kwargs : dict, optional
optional keyword arguments
wl : float or str, optional
wavelength of the x-rays in angstrom
en : float or str, optional
energy of the x-rays in electronvolt
UB : array-like, optional
matrix for conversion from (hkl) coordinates to Q of sample used to
determine not Q but (hkl) (default: identity matrix)
"""
utilities.check_kwargs(kwargs, self._valid_init_kwargs,
self.__class__.__name__)
# initialize some needed variables
self._kappa_dir = numpy.array((numpy.nan, numpy.nan, numpy.nan))
# set/check sample and detector axis geometry
self._set_sampleAxis(sampleAxis)
self._set_detectorAxis(detectorAxis)
# r_i: primary beam direction
if isinstance(r_i, (list, tuple, numpy.ndarray)):
self.r_i = numpy.array(r_i, dtype=numpy.double)
if self.r_i.size != 3:
print("XU.QConversion: warning invalid primary beam "
"direction given -> using [0, 1, 0]")
self.r_i = numpy.array([0, 1, 0], dtype=numpy.double)
else:
raise TypeError("QConversion: invalid type of primary beam "
"direction r_i, must be tuple, list or "
"numpy.ndarray")
# kwargs
self._set_wavelength(kwargs.get("wl", config.WAVELENGTH))
if "en" in kwargs:
self._set_energy(kwargs["en"])
self._set_UB(kwargs.get('UB', numpy.identity(3)))
self._linear_init = False
self._area_init = False
self._area_detrotaxis_set = False
def _set_energy(self, energy):
self._en = utilities.energy(energy)
self._wl = utilities.en2lam(self._en)
def _set_wavelength(self, wl):
self._wl = utilities.wavelength(wl)
self._en = utilities.lam2en(self._wl)
def _get_energy(self):
return self._en
def _get_wavelength(self):
return self._wl
def _set_sampleAxis(self, sampleAxis):
"""
property handler for _sampleAxis
checks if a syntactically correct list of sample circles is given
Parameters
----------
sampleAxis : list or tuple
sample circles, e.g. ['x+', 'z+']
"""
if isinstance(sampleAxis, (str, list, tuple)):
if isinstance(sampleAxis, str):
sAxis = list([sampleAxis])
else:
sAxis = list(sampleAxis)
for circ in sAxis:
if not isinstance(circ, str) or len(circ) != 2:
raise InputError("QConversion: incorrect sample circle "
"type or syntax (%s)" % repr(circ))
if not circleSyntaxSample.search(circ):
raise InputError("QConversion: incorrect sample circle "
"syntax (%s)" % circ)
if circ[0] == 'k': # determine kappa rotation axis
self._kappa_dir = math.getVector(circ)
if config.VERBOSITY >= config.DEBUG:
print("XU.QConversion: kappa_dir: (%5.3f %5.3f %5.3f)"
% tuple(self._kappa_dir))
else:
raise TypeError("Qconversion error: invalid type for sampleAxis, "
"must be str, list, or tuple")
self._sampleAxis = sAxis
self._sampleAxis_str = ''
for circ in self._sampleAxis:
self._sampleAxis_str += circ
def _get_sampleAxis(self):
"""
property handler for _sampleAxis
Returns
-------
list
sample axes following the syntax /[xyzk][+-]/
"""
return self._sampleAxis
def _set_detectorAxis(self, detectorAxis, detrot=False):
"""
property handler for _detectorAxis_
checks if a syntactically correct list of detector circles is given
Parameters
----------
detectorAxis : list or tuple
detector circles, e.g. ['x+']
detrot : bool, optional
flag to tell that the detector rotation is going to be added (used
internally to avoid double adding of detector rotation axis)
"""
has_translations = False
if isinstance(detectorAxis, (str, list, tuple)):
if isinstance(detectorAxis, str):
dAxis = list([detectorAxis])
else:
dAxis = list(detectorAxis)
for circ in dAxis:
if not isinstance(circ, str) or len(circ) != 2:
raise InputError("QConversion: incorrect detector circle "
"type or syntax (%s)" % repr(circ))
if not circleSyntaxDetector.search(circ):
raise InputError("QConversion: incorrect detector circle "
"syntax (%s)" % circ)
if circ[0] == 't':
has_translations = True
else:
raise TypeError("Qconversion error: invalid type for "
"detectorAxis, must be str, list or tuple")
self._detectorAxis = dAxis
self._detectorAxis_str = ''
self._has_translations = has_translations
for circ in self._detectorAxis:
self._detectorAxis_str += circ
if detrot:
self._area_detrotaxis_set = True
else:
self._area_init = False
def _get_detectorAxis(self):
"""
property handler for _detectorAxis
Returns
-------
list of detector axis following the syntax /[xyz][+-]/
"""
return self._detectorAxis
def _get_UB(self):
return self._UB
def _set_UB(self, UB):
"""
set the orientation matrix used in the Qconversion
needs to be (3, 3) matrix
"""
tmp = numpy.array(UB)
if tmp.shape != (3, 3) and tmp.size != 9:
raise InputError("QConversion: incorrect shape of UB matrix "
"(shape: %s)" % str(tmp.shape))
self._UB = tmp.reshape((3, 3))
energy = property(_get_energy, _set_energy)
wavelength = property(_get_wavelength, _set_wavelength)
sampleAxis = property(_get_sampleAxis, _set_sampleAxis)
detectorAxis = property(_get_detectorAxis, _set_detectorAxis)
UB = property(_get_UB, _set_UB)
def __str__(self):
pstr = 'QConversion geometry \n'
pstr += '---------------------------\n'
pstr += f'sample geometry({len(self._sampleAxis)}): ' + \
self._sampleAxis_str + '\n'
if self._sampleAxis_str.find('k') != -1:
pstr += ('kappa rotation axis (%5.3f %5.3f %5.3f)\n'
% tuple(self._kappa_dir))
pstr += f'detector geometry({len(self._detectorAxis)}): ' + \
self._detectorAxis_str + '\n'
pstr += ('primary beam direction: (%5.2f %5.2f %5.2f) \n'
% (self.r_i[0], self.r_i[1], self.r_i[2]))
if self._linear_init:
pstr += '\n linear detector initialized:\n'
pstr += 'linear detector mount direction: ' + \
self._linear_detdir + '\n'
pstr += ('number of channels/center channel: %d/%d\n'
% (self._linear_Nch, self._linear_cch))
pstr += ('distance to center of rotation/pixel width: '
'%10.4g/%10.4g\n'
% (self._linear_distance, self._linear_pixwidth))
chpdeg = 2 * self._linear_distance / \
self._linear_pixwidth * numpy.tan(numpy.radians(0.5))
pstr += f'corresponds to channel per degree: {chpdeg:8.2f}\n'
if self._area_init:
pstr += '\n area detector initialized:\n'
pstr += 'area detector mount directions: %s/%s\n' % (
self._area_detdir1, self._area_detdir2)
pstr += ('number of channels/center channels: (%d,%d) / (%d,%d)\n'
% (self._area_Nch1, self._area_Nch2,
self._area_cch1, self._area_cch2))
pstr += ('distance to center of rotation/pixel width: '
'%10.4g/ (%10.4g,%10.4g) \n'
% (self._area_distance, self._area_pwidth1,
self._area_pwidth2))
chpdeg1 = 2 * self._area_distance / \
self._area_pwidth1 * numpy.tan(numpy.radians(0.5))
chpdeg2 = 2 * self._area_distance / \
self._area_pwidth2 * numpy.tan(numpy.radians(0.5))
pstr += 'corresponds to channel per degree: (%8.2f,%8.2f)\n' % (
chpdeg1, chpdeg2)
return pstr
def _checkInput(self, *args):
"""
helper function to check that the arguments given to the QConversion
routines have the correct shape. It determines the number of points in
the input from the longest array/list given and checks that only inputs
combatible with this length are given
Parameters
----------
args : list
arguments from the QConversion routine (sample and detector angles)
Returns
-------
Npoints : int
integer to tell the number of points given
"""
np = 1
for a in args:
# optain size of input
if isinstance(a, numpy.ndarray):
anp = a.size
elif isinstance(a, (list, tuple)):
anp = len(a)
elif isinstance(a, numbers.Number):
anp = 1
else:
raise TypeError('QConversion: Input argument #%d has an '
'invalid type.' % args.index(a))
# check if the input field is valid
if anp > 1 and np == 1:
np = anp
elif anp > 1 and np != anp:
raise InputError('QConversion: Several input-arrays arguments '
'with different shape are an invalid input!')
return np
def _reshapeInput(self, npoints, delta, circles, *args, **kwargs):
"""
helper function to reshape the input of arguments to
(len(args), npoints) The input arguments must be either scalars or are
of length npoints.
Parameters
----------
npoints : int
length of the input arrays
delta : list or array-like
value to substract from the input arguments as array with len(args)
circles : list
list of circle description to decide if degree/radians conversion
is needed
args : list
input arrays and scalars
kwargs : dict, optional
optional keyword argument to tell if values of rotation axis should
be converted to radiants (name= 'deg', default=True)
Returns
-------
inarr : ndarray
array of shape (len(args), npoints) with the input arguments
retshape : tuple
shape of return values
"""
inarr = numpy.empty((len(args), npoints), dtype=numpy.double)
retshape = (npoints,) # default value
deg2rad = kwargs.get('deg', True)
for i in range(len(args)):
arg = args[i]
if not isinstance(arg,
(numbers.Number, list, tuple, numpy.ndarray)):
raise TypeError("QConversion: invalid type for one of the "
"sample coordinates, must be scalar, list or "
"array")
if isinstance(arg, numbers.Number):
arg = numpy.ones(npoints, dtype=numpy.double) * arg
elif isinstance(arg, (list, tuple)):
arg = numpy.array(arg, dtype=numpy.double)
else: # determine return value shape
retshape = arg.shape
arg = arg - delta[i]
if deg2rad and circleSyntaxSample.search(circles[i]):
inarr[i, :] = numpy.radians(numpy.ravel(arg))
else:
inarr[i, :] = numpy.ravel(arg)
return inarr, retshape
def _parse_common_kwargs(self, **kwargs):
"""
parse common keyword arguments to QConversion calls
Parameters
----------
delta : list or array-like, optional
delta angles to correct the given ones for misalignment.
delta must be an numpy array or list of len(*args). used angles are
than ``*args - delta``
UB : array-like, optional
matrix for conversion from (hkl) coordinates to Q of sample used to
determine not Q but (hkl) (default: self.UB)
wl : float or str, optional
x-ray wavelength in angstrom (default: self._wl)
en : float, optional
x-ray energy in eV (default is converted self._wl). both wavelength
and energy can also be an array which enables the QConversion for
energy scans. Note that the `en` keyword overrules the `wl`
keyword!
deg : bool, optional
flag to tell if angles are passed as degree (default: True)
sampledis : tuple or list or array-like
sample displacement vector in relative units of the detector
distance. Applies to parallel beam geometry. (default: (0, 0, 0))
"""
flags = QConvFlags.NONE
if self._has_translations:
flags |= QConvFlags.HAS_TRANSLATIONS
Ns = len(self.sampleAxis)
Nd = len(self.detectorAxis)
if self._area_detrotaxis_set:
Nd -= 1 # do not consider detector rotation for point detector
Ncirc = Ns + Nd
# kwargs
wl = utilities.wavelength(kwargs.get('wl', self._wl))
if 'en' in kwargs:
wl = utilities.lam2en(utilities.energy(kwargs['en']))
deg = kwargs.get('deg', True)
delta = numpy.asarray(kwargs.get('delta', numpy.zeros(Ncirc)),
dtype=numpy.double)
if delta.size != Ncirc:
raise InputError("QConversion: keyword argument delta does "
"not have an appropriate shape")
UB = numpy.asarray(kwargs.get('UB', self.UB))
sd = numpy.asarray(kwargs.get('sampledis', [0, 0, 0]))
if 'sampledis' in kwargs:
flags |= QConvFlags.HAS_SAMPLEDIS
return Ns, Nd, Ncirc, wl, deg, delta, UB, sd, flags
[docs]
def __call__(self, *args, **kwargs):
"""
wrapper function for point(...)
"""
return self.point(*args, **kwargs)
[docs]
def point(self, *args, **kwargs):
"""
angular to momentum space conversion for a point detector
located in direction of self.r_i when detector angles are zero
Parameters
----------
args : ndarray, list or Scalars
sample and detector angles; in total `len(self.sampleAxis) +
len(detectorAxis)` must be given, always starting with the outer
most circle. all arguments must have the same shape or length but
can be mixed with Scalars (i.e. if an angle is always the same it
can be given only once instead of an array)
- sAngles :
sample circle angles, number of arguments must correspond to
len(self.sampleAxis)
- dAngles :
detector circle angles, number of arguments must correspond to
len(self.detectorAxis)
kwargs : dict, optional
optional keyword arguments
delta : list or array-like, optional
delta angles to correct the given ones for misalignment.
delta must be an numpy array or list of ``len(*args)``. used angles
are then ``*args - delta``
UB : array-like, optional
matrix for conversion from (hkl) coordinates to Q of sample used to
determine not Q but (hkl) (default: self.UB)
wl : float or str, optional
x-ray wavelength in angstrom (default: self._wl)
en : float, optional
x-ray energy in eV (default is converted self._wl). both wavelength
and energy can also be an array which enables the QConversion for
energy scans. Note that the `en` keyword overrules the `wl`
keyword!
deg : bool, optional
flag to tell if angles are passed as degree (default: True)
sampledis : tuple or list or array-like
sample displacement vector in relative units of the detector
distance. Applies to parallel beam geometry. (default: (0, 0, 0))
Returns
-------
ndarray
reciprocal space positions as numpy.ndarray with shape ``(N , 3)``
where `N` corresponds to the number of points given in the input
"""
utilities.check_kwargs(kwargs, self._valid_call_kwargs, 'Ang2Q/point')
Ns, Nd, Ncirc, wl, deg, delta, UB, sd, flags = \
self._parse_common_kwargs(**kwargs)
# prepare angular arrays from *args
# need one sample angle and one detector angle array
if len(args) != Ncirc:
raise InputError("QConversion: wrong amount (%d) of arguments "
"given, number of arguments should be %d"
% (len(args), Ncirc))
# determine the number of points
a = args + (wl,)
Npoints = self._checkInput(*a)
# reshape/recast input arguments for sample and detector angles
sAngles, retshape = self._reshapeInput(Npoints, delta[:Ns],
self.sampleAxis, *args[:Ns],
deg=deg)
dAngles = self._reshapeInput(Npoints, delta[Ns:],
self.detectorAxis, *args[Ns:],
deg=deg)[0]
wl = numpy.ravel(self._reshapeInput(Npoints, (0, ), 'a',
wl, deg=False)[0])
sAngles = sAngles.transpose()
dAngles = dAngles.transpose()
sAxis = self._sampleAxis_str
dAxis = self._detectorAxis_str
if self._area_detrotaxis_set:
# do not consider detector rotation for point detector
dAxis = self._detectorAxis_str[:-2]
else:
dAxis = self._detectorAxis_str
if config.VERBOSITY >= config.DEBUG:
print("XU.QConversion: Ns, Nd: %d %d" % (Ns, Nd))
print(f"XU.QConversion: sAngles / dAngles {str(sAngles)} / "
f"{str(dAngles)}")
qpos = cxrayutilities.ang2q_conversion(
sAngles, dAngles, self.r_i, sAxis, dAxis,
self._kappa_dir, UB, sd, wl, config.NTHREADS, flags)
if Npoints == 1:
return (qpos[0, 0], qpos[0, 1], qpos[0, 2])
return numpy.reshape(qpos[:, 0], retshape), \
numpy.reshape(qpos[:, 1], retshape), \
numpy.reshape(qpos[:, 2], retshape)
[docs]
def init_linear(self, detectorDir, cch, Nchannel, distance=None,
pixelwidth=None, chpdeg=None, tilt=0, **kwargs):
"""
initialization routine for linear detectors
detector direction as well as distance and pixel size or
channels per degree must be given.
Parameters
----------
detectorDir : str
direction of the detector (along the pixel array); e.g. 'z+'
cch : float
center channel, in direction of self.r_i at zero detectorAngles
Nchannel : int
total number of detector channels
distance : float, optional
distance of center channel from center of rotation
pixelwidth : float, optional
width of one pixel (same unit as distance)
chpdeg : float, optional
channels per degree (only absolute value is relevant) sign
determined through detectorDir
tilt : float, optional
tilt of the detector axis from the detectorDir (in degree)
kwargs: dict, optional
optional keyword arguments
Nav : int, optional
number of channels to average to reduce data size (default: 1)
roi : tuple or list
region of interest for the detector pixels; e.g. [100, 900]
Note:
Either distance and pixelwidth or chpdeg must be given !!
Note:
the channel numbers run from 0 .. Nchannel-1
"""
utilities.check_kwargs(kwargs, self._valid_linear_kwargs,
'init_linear')
# detectorDir
if not isinstance(detectorDir, str) or len(detectorDir) != 2:
raise InputError("QConversion: incorrect detector direction type "
"or syntax (%s)" % repr(detectorDir))
if not directionSyntax.search(detectorDir):
raise InputError("QConversion: incorrect detector direction "
"syntax (%s)" % detectorDir)
self._linear_detdir = detectorDir
self._linear_Nch = int(Nchannel)
self._linear_cch = float(cch)
self._linear_tilt = numpy.radians(tilt)
if distance is not None and pixelwidth is not None:
self._linear_distance = float(distance)
self._linear_pixwidth = float(pixelwidth)
elif chpdeg is not None:
self._linear_distance = 1.0
self._linear_pixwidth = 2 * self._linear_distance / \
numpy.abs(float(chpdeg)) * numpy.tan(numpy.radians(0.5))
else:
# not all needed values were given
raise InputError("QConversion: not all mandatory arguments were "
"given -> read API doc, need distance and "
"pixelwidth or chpdeg")
# kwargs
self._linear_roi = kwargs.get('roi', [0, self._linear_Nch])
self._linear_nav = kwargs.get('Nav', 1)
# rescale r_i
self.r_i = math.VecUnit(self.r_i) * self._linear_distance
self._linear_init = True
def _get_detparam_linear(self, oroi, nav):
"""
initialize linear detector geometry for C subroutines. This function
considers the Nav and roi options.
"""
cch = self._linear_cch / float(nav)
pwidth = self._linear_pixwidth * nav
roi = numpy.array(oroi)
roi[0] = numpy.floor(oroi[0] / float(nav))
roi[1] = numpy.ceil((oroi[1] - oroi[0]) / float(nav)) + roi[0]
roi = roi.astype(numpy.int32)
return cch, pwidth, roi
[docs]
def linear(self, *args, **kwargs):
"""
angular to momentum space conversion for a linear detector
the cch of the detector must be in direction of self.r_i when
detector angles are zero
the detector geometry must be initialized by the init_linear(...)
routine
Parameters
----------
args : ndarray, list or Scalars
sample and detector angles; in total `len(self.sampleAxis) +
len(detectorAxis)` must be given, always starting with the outer
most circle. all arguments must have the same shape or length but
can be mixed with Scalars (i.e. if an angle is always the same it
can be given only once instead of an array)
- sAngles :
sample circle angles, number of arguments must correspond to
len(self.sampleAxis)
- dAngles :
detector circle angles, number of arguments must correspond to
len(self.detectorAxis)
kwargs : dict, optional
optional keyword arguments
delta : list or array-like, optional
delta angles to correct the given ones for misalignment.
delta must be an numpy array or list of ``len(*args)``. used angles
are then ``*args - delta``
UB : array-like, optional
matrix for conversion from (hkl) coordinates to Q of sample used to
determine not Q but (hkl) (default: self.UB)
Nav : int, optional
number of channels to average to reduce data size (default:
self._linear_nav)
roi : list or tuple, optional
region of interest for the detector pixels; e.g. [100, 900]
(default: self._linear_roi)
wl : float or str, optional
x-ray wavelength in angstrom (default: self._wl)
en : float, optional
x-ray energy in eV (default is converted self._wl). both wavelength
and energy can also be an array which enables the QConversion for
energy scans. Note that the `en` keyword overrules the `wl`
keyword!
deg : bool, optional
flag to tell if angles are passed as degree (default: True)
sampledis : tuple or list or array-like
sample displacement vector in relative units of the detector
distance. Applies to parallel beam geometry. (default: (0, 0, 0))
Returns
-------
reciprocal space position of all detector pixels in a numpy.ndarray of
shape ( (*)*(self._linear_roi[1]-self._linear_roi[0]+1) , 3 )
"""
if not self._linear_init:
raise UsageError("QConversion: linear detector not initialized -> "
"call Ang2Q.init_linear(...)")
valid_kwargs = copy.copy(self._valid_call_kwargs)
valid_kwargs.update(self._valid_linear_kwargs)
utilities.check_kwargs(kwargs, valid_kwargs, 'Ang2Q/linear')
Ns, _, Ncirc, wl, deg, delta, UB, sd, flags = \
self._parse_common_kwargs(**kwargs)
# extra keyword arguments
nav = kwargs.get('Nav', self._linear_nav)
oroi = kwargs.get('roi', self._linear_roi)
# prepare angular arrays from *args
# need one sample angle and one detector angle array
if len(args) != Ncirc:
raise InputError("QConversion: wrong amount (%d) of arguments "
"given, number of arguments should be %d"
% (len(args), Ncirc))
# determine the number of points
a = args + (wl,)
Npoints = self._checkInput(*a)
# reshape/recast input arguments for sample and detector angles
sAngles = self._reshapeInput(Npoints, delta[:Ns],
self.sampleAxis, *args[:Ns], deg=deg)[0]
dAngles = self._reshapeInput(Npoints, delta[Ns:],
self.detectorAxis, *args[Ns:], deg=deg)[0]
wl = numpy.ravel(
self._reshapeInput(Npoints, (0, ), 'a', wl, deg=False)[0])
sAngles = sAngles.transpose()
dAngles = dAngles.transpose()
cch, pwidth, roi = self._get_detparam_linear(oroi, nav)
sAxis = self._sampleAxis_str
dAxis = self._detectorAxis_str
qpos = cxrayutilities.ang2q_conversion_linear(
sAngles, dAngles, self.r_i, sAxis, dAxis, self._kappa_dir,
cch, pwidth, roi, self._linear_detdir, self._linear_tilt,
UB, sd, wl, config.NTHREADS, flags)
# reshape output
if Npoints == 1:
qpos.shape = (Npoints * (roi[1] - roi[0]), 3)
return qpos[:, 0], qpos[:, 1], qpos[:, 2]
qpos.shape = (Npoints, (roi[1] - roi[0]), 3)
return qpos[:, :, 0], qpos[:, :, 1], qpos[:, :, 2]
[docs]
def init_area(self, detectorDir1, detectorDir2, cch1, cch2, Nch1, Nch2,
distance=None, pwidth1=None, pwidth2=None, chpdeg1=None,
chpdeg2=None, detrot=0, tiltazimuth=0, tilt=0, **kwargs):
"""
initialization routine for area detectors
detector direction as well as distance and pixel size or
channels per degree must be given. Two separate pixel sizes and
channels per degree for the two orthogonal directions can be given
Parameters
----------
detectorDir1 : str
direction of the detector (along the pixel direction 1); e.g. 'z+'
means higher pixel numbers at larger z positions
detectorDir2 : str
direction of the detector (along the pixel direction 2); e.g. 'x+'
cch1, cch2 : float
center pixel, in direction of self.r_i at zero detectorAngles
Nch1, Nch2 : int
number of detector pixels along direction 1, 2
distance : float, optional
distance of center pixel from center of rotation
pwidth1, pwidth2 : float, optional
width of one pixel (same unit as distance)
chpdeg1, chpdeg2 : float, optional
channels per degree (only absolute value is relevant) sign
determined through `detectorDir1, detectorDir2`
detrot : float, optional
angle of the detector rotation around primary beam direction (used
to correct misalignments)
tiltazimuth : float, optional
direction of the tilt vector in the detector plane (in degree)
tilt : float, optional
tilt of the detector plane around an axis normal to the direction
given by the tiltazimuth
kwargs : dict, optional
optional keyword arguments
Nav : tuple or list, optional
number of channels to average to reduce data size (default: [1, 1])
roi : tuple or list, optional
region of interest for the detector pixels; e.g.
[100, 900, 200, 800]
Note:
Either distance and pwidth1, pwidth2 or chpdeg1, chpdeg2 must
be given !!
Note:
the channel numbers run from 0 .. NchX-1
"""
utilities.check_kwargs(kwargs, self._valid_linear_kwargs, 'init_area')
# detectorDir
if not isinstance(detectorDir1, str) or len(detectorDir1) != 2:
raise InputError("QConversion: incorrect detector direction1 type "
"or syntax (%s)" % repr(detectorDir1))
if not directionSyntax.search(detectorDir1):
raise InputError("QConversion: incorrect detector direction1 "
"syntax (%s)" % detectorDir1)
self._area_detdir1 = detectorDir1
if not isinstance(detectorDir2, str) or len(detectorDir2) != 2:
raise InputError("QConversion: incorrect detector direction2 type "
"or syntax (%s)" % repr(detectorDir2))
if not directionSyntax.search(detectorDir2):
raise InputError("QConversion: incorrect detector direction2 "
"syntax (%s)" % detectorDir2)
self._area_detdir2 = detectorDir2
# other none keyword arguments
self._area_Nch1 = int(Nch1)
self._area_Nch2 = int(Nch2)
self._area_cch1 = float(cch1)
self._area_cch2 = float(cch2)
# if detector rotation is present add new motor to consider it in
# conversion
self._area_detrot = numpy.radians(detrot)
if self._area_detrot != 0.:
if self._area_detrotaxis_set:
self._set_detectorAxis(
self._get_detectorAxis()[:-1] + [math.getSyntax(self.r_i)],
detrot=True)
else:
self._set_detectorAxis(
self._get_detectorAxis() + [math.getSyntax(self.r_i)],
detrot=True)
self._area_tiltazimuth = numpy.radians(tiltazimuth)
self._area_tilt = numpy.radians(tilt)
# mandatory keyword arguments
if (distance is not None and pwidth1 is not None and
pwidth2 is not None):
self._area_distance = float(distance)
self._area_pwidth1 = float(pwidth1)
self._area_pwidth2 = float(pwidth2)
elif chpdeg1 is not None and chpdeg2 is not None:
self._area_distance = 1.0
self._area_pwidth1 = 2 * self._area_distance / \
numpy.abs(float(chpdeg1)) * numpy.tan(numpy.radians(0.5))
self._area_pwidth2 = 2 * self._area_distance / \
numpy.abs(float(chpdeg2)) * numpy.tan(numpy.radians(0.5))
else:
# not all needed values were given
raise InputError("Qconversion error: not all mandatory arguments "
"were given -> read API doc")
# kwargs
self._area_roi = kwargs.get('roi', [0, self._area_Nch1,
0, self._area_Nch2])
self._area_nav = kwargs.get('Nav', [1, 1])
# rescale r_i
self.r_i = math.VecUnit(self.r_i) * self._area_distance
self._area_init = True
def _get_detparam_area(self, oroi, nav):
"""
initialize CCD geomtry for C subroutines. This function considers the
Nav and roi options.
"""
cch1 = self._area_cch1 / float(nav[0])
cch2 = self._area_cch2 / float(nav[1])
pwidth1 = self._area_pwidth1 * nav[0]
pwidth2 = self._area_pwidth2 * nav[1]
roi = numpy.array(oroi)
roi[0] = numpy.floor(oroi[0] / float(nav[0]))
roi[1] = numpy.ceil((oroi[1] - oroi[0]) / float(nav[0])) + roi[0]
roi[2] = numpy.floor(oroi[2] / float(nav[1]))
roi[3] = numpy.ceil((oroi[3] - oroi[2]) / float(nav[1])) + roi[2]
roi = roi.astype(numpy.int32)
return cch1, cch2, pwidth1, pwidth2, roi
[docs]
def area(self, *args, **kwargs):
"""
angular to momentum space conversion for a area detector
the center pixel defined by the init_area routine must be
in direction of self.r_i when detector angles are zero
the detector geometry must be initialized by the init_area(...) routine
Parameters
----------
args : ndarray, list or Scalars
sample and detector angles; in total `len(self.sampleAxis) +
len(detectorAxis)` must be given, always starting with the outer
most circle. all arguments must have the same shape or length but
can be mixed with Scalars (i.e. if an angle is always the same it
can be given only once instead of an array)
- sAngles :
sample circle angles, number of arguments must correspond to
len(self.sampleAxis)
- dAngles :
detector circle angles, number of arguments must correspond to
len(self.detectorAxis)
kwargs : dict, optional
optional keyword arguments
delta : list or array-like, optional
delta angles to correct the given ones for misalignment.
delta must be an numpy array or list of ``len(*args)``. used angles
are then ``*args - delta``
UB : array-like, optional
matrix for conversion from (hkl) coordinates to Q of sample used to
determine not Q but (hkl) (default: self.UB)
Nav : tuple or list, optional
number of channels to average to reduce data size e.g. [2, 2]
(default: self._area_nav)
roi : list or tuple, optional
region of interest for the detector pixels; e.g.
[100, 900, 200, 800] (default: self._area_roi)
wl : float or str, optional
x-ray wavelength in angstrom (default: self._wl)
en : float, optional
x-ray energy in eV (default is converted self._wl). both wavelength
and energy can also be an array which enables the QConversion for
energy scans. Note that the `en` keyword overrules the `wl`
keyword!
deg : bool, optional
flag to tell if angles are passed as degree (default: True)
sampledis : tuple or list or array-like
sample displacement vector in relative units of the detector
distance. Applies to parallel beam geometry. (default: (0, 0, 0))
Returns
-------
reciprocal space position of all detector pixels in a numpy.ndarray of
shape ((*)*(self._area_roi[1] - self._area_roi[0]+1) *
(self._area_roi[3] - self._area_roi[2] + 1) , 3) were detectorDir1 is
the fastest varing
"""
if not self._area_init:
raise UsageError("QConversion: area detector not initialized -> "
"call Ang2Q.init_area(...)")
valid_kwargs = copy.copy(self._valid_call_kwargs)
valid_kwargs.update(self._valid_linear_kwargs)
utilities.check_kwargs(kwargs, valid_kwargs, 'Ang2Q/area')
Ns, Nd, Ncirc, wl, deg, delta, UB, sd, flags = \
self._parse_common_kwargs(**kwargs)
# extra keyword arguments
nav = kwargs.get('Nav', self._area_nav)
oroi = kwargs.get('roi', self._area_roi)
# prepare angular arrays from *args
# need one sample angle and one detector angle array
if len(args) != Ncirc:
raise InputError("QConversion: wrong amount (%d) of arguments "
"given, number of arguments should be %d"
% (len(args), Ncirc))
# determine the number of points
a = args + (wl,)
Npoints = self._checkInput(*a)
# reshape/recast input arguments for sample and detector angles
sAngles = self._reshapeInput(Npoints, delta[:Ns],
self.sampleAxis, *args[:Ns], deg=deg)[0]
wl = numpy.ravel(
self._reshapeInput(Npoints, (0, ), 'a', wl, deg=False)[0])
if self._area_detrotaxis_set:
Nd = Nd + 1
if deg:
a = args[Ns:] + (numpy.degrees(self._area_detrot),)
else:
a = args[Ns:] + (self._area_detrot,)
dAngles = self._reshapeInput(
Npoints, numpy.append(delta[Ns:], 0),
self.detectorAxis, *a, deg=deg)[0]
else:
dAngles = self._reshapeInput(Npoints, delta[Ns:],
self.detectorAxis, *args[Ns:],
deg=deg)[0]
sAngles = sAngles.transpose()
dAngles = dAngles.transpose()
cch1, cch2, pwidth1, pwidth2, roi = self._get_detparam_area(oroi, nav)
if config.VERBOSITY >= config.DEBUG:
print("QConversion.area: roi, number of points per frame: %s, %d"
% (str(roi), (roi[1] - roi[0]) * (roi[3] - roi[2])))
print(f"QConversion.area: cch1, cch2: {cch1:5.2f} {cch2:5.2f}")
sAxis = self._sampleAxis_str
dAxis = self._detectorAxis_str
qpos = cxrayutilities.ang2q_conversion_area(
sAngles, dAngles, self.r_i, sAxis, dAxis, self._kappa_dir,
cch1, cch2, pwidth1, pwidth2, roi, self._area_detdir1,
self._area_detdir2, self._area_tiltazimuth, self._area_tilt,
UB, sd, wl, config.NTHREADS, flags)
# reshape output
if Npoints == 1:
qpos.shape = ((roi[1] - roi[0]), (roi[3] - roi[2]), 3)
return qpos[:, :, 0], qpos[:, :, 1], qpos[:, :, 2]
qpos.shape = (Npoints, (roi[1] - roi[0]), (roi[3] - roi[2]), 3)
return qpos[:, :, :, 0], qpos[:, :, :, 1], qpos[:, :, :, 2]
[docs]
def getDetectorPos(self, *args, **kwargs):
"""
obtains the detector position vector by applying the detector arm
rotations.
Parameters
----------
args : list
detector angles. Only detector arm angles as described by the
detectorAxis attribute must be given.
kwargs : dict, optional
optional keyword arguments
dim : int, optional
dimension of the detector for which the position should be
determined
roi : tuple or list, optional
region of interest for the detector pixels; (default:
self._area_roi/self._linear_roi)
Nav : tuple or list, optional
number of channels to average to reduce data size; (default:
self._area_nav/self._linear_nav)
deg : bool, optional
flag to tell if angles are passed as degree (default: True)
Returns
-------
ndarray
numpy array of length 3 with vector components of the detector
direction. The length of the vector is k.
"""
valid_kwargs = copy.copy(self._valid_linear_kwargs)
valid_kwargs['dim'] = 'dimensionality of the detector'
valid_kwargs['deg'] = 'True if angles are in degrees'
utilities.check_kwargs(kwargs, valid_kwargs, 'get_detector_pos')
dim = kwargs.get('dim', 0)
if dim == 1 and not self._linear_init:
raise UsageError("QConversion: linear detector not initialized -> "
"call Ang2Q.init_linear(...)")
if dim == 2 and not self._area_init:
raise UsageError("QConversion: area detector not initialized -> "
"call Ang2Q.init_area(...)")
Nd = len(self.detectorAxis)
if self._area_detrotaxis_set:
Nd = Nd - 1
# kwargs
deg = kwargs.get('deg', True)
# prepare angular arrays from *args
# need one sample angle and one detector angle array
if len(args) != Nd:
raise InputError(f"QConversion: wrong amount ({len(args)}) of "
"arguments given, number of arguments should be "
f"{Nd}")
# determine the number of points and reshape input arguments
Npoints = self._checkInput(*args)
if dim == 2 and self._area_detrotaxis_set:
Nd = Nd + 1
if deg:
a = args + (numpy.degrees(self._area_detrot),)
else:
a = args + (self._area_detrot,)
dAngles, retshape = self._reshapeInput(
Npoints, numpy.append(numpy.zeros(Nd), 0),
self.detectorAxis, *a, deg=deg)
else:
dAngles, retshape = self._reshapeInput(Npoints, numpy.zeros(Nd),
self.detectorAxis, *args,
deg=deg)
dAngles = dAngles.transpose()
dAxis = self._detectorAxis_str
if dim == 2:
oroi = kwargs.get('roi', self._area_roi)
nav = kwargs.get('Nav', self._area_nav)
cch1, cch2, pwidth1, pwidth2, roi = self._get_detparam_area(oroi,
nav)
cfunc = cxrayutilities.ang2q_detpos_area
dpos = cfunc(dAngles, self.r_i, dAxis, cch1, cch2, pwidth1,
pwidth2, roi, self._area_detdir1, self._area_detdir2,
self._area_tiltazimuth, self._area_tilt,
config.NTHREADS)
# reshape output
if Npoints == 1:
dpos.shape = ((roi[1] - roi[0]), (roi[3] - roi[2]), 3)
return dpos[:, :, 0], dpos[:, :, 1], dpos[:, :, 2]
dpos.shape = (Npoints, (roi[1] - roi[0]), (roi[3] - roi[2]), 3)
return dpos[:, :, :, 0], dpos[:, :, :, 1], dpos[:, :, :, 2]
if dim == 1:
oroi = kwargs.get('roi', self._linear_roi)
nav = kwargs.get('Nav', self._linear_nav)
cch, pwidth, roi = self._get_detparam_linear(oroi, nav)
cfunc = cxrayutilities.ang2q_detpos_linear
dpos = cfunc(dAngles, self.r_i, dAxis, cch, pwidth, roi,
self._linear_detdir, self._linear_tilt,
config.NTHREADS)
# reshape output
if Npoints == 1:
dpos.shape = (Npoints * (roi[1] - roi[0]), 3)
return dpos[:, 0], dpos[:, 1], dpos[:, 2]
dpos.shape = (Npoints, (roi[1] - roi[0]), 3)
return dpos[:, :, 0], dpos[:, :, 1], dpos[:, :, 2]
cfunc = cxrayutilities.ang2q_detpos
dpos = cfunc(dAngles, self.r_i, dAxis, config.NTHREADS)
if Npoints == 1:
return (dpos[0, 0], dpos[0, 1], dpos[0, 2])
return numpy.reshape(dpos[:, 0], retshape), \
numpy.reshape(dpos[:, 1], retshape), \
numpy.reshape(dpos[:, 2], retshape)
[docs]
def getDetectorDistance(self, *args, **kwargs):
"""
obtains the detector distance by applying the detector arm movements.
This is especially interesting for the case of 1 or 2D detectors to
perform certain geometric corrections.
Parameters
----------
args : list
detector angles. Only detector arm angles as described by the
detectorAxis attribute must be given.
kwargs : dict, optional
optional keyword arguments
dim : int, optional
dimension of the detector for which the position should be
determined
roi : tuple or list, optional
region of interest for the detector pixels; (default:
self._area_roi/self._linear_roi)
Nav : tuple or list, optional
number of channels to average to reduce data size; (default:
self._area_nav/self._linear_nav)
deg : bool, optional
flag to tell if angles are passed as degree (default: True)
Returns
-------
ndarray
numpy array with the detector distance
"""
x, y, z = self.getDetectorPos(*args, **kwargs)
return numpy.sqrt(x**2 + y**2 + z**2)
[docs]
class Experiment:
"""
base class for describing experiments
users should use the derived classes: HXRD, GID, PowderExperiment
"""
_valid_init_kwargs = {'en': 'x-ray energy',
'wl': 'x-ray wavelength',
'qconv': 'reciprocal space conversion',
'sampleor': 'sample orientation'}
[docs]
def __init__(self, ipdir, ndir, **keyargs):
"""
initialization of an Experiment class needs the sample orientation
given by the samples surface normal and an second not colinear
direction specifying the inplane reference direction in the crystal
coordinate system. The orientation of the surface normal in the lab
coordinate system can also be given or is automatically determined by
the goniometer type (see argument sampleor).
Parameters
----------
ipdir : list or tuple or array-like
inplane reference direction (ipdir points into the primary beam
direction at zero angles)
ndir : list or tuple or array-like
surface normal of your sample. ndir points in a direction
perpendicular to the primary beam, how it is orientated in real
space is determined by the parameter sampleor (see below).
keyargs : dict, optional
optional keyword arguments
qconv : QConversion, optional
QConversion object to use for the Ang2Q conversion
sampleor : {'det', 'sam', '[xyz][+-]'}, optional
determines the sample surface orientation with respect to the
coordinate system in which the goniometer rotations are given. You
can use the [xyz][+-] syntax to specify the nominal surface
orientation (when all goniometer angles are zero). In addition two
special values 'det' and 'sam' are available, which will let the
code determine the orientation from either the inner most detector
or sample rotation. 'det' means the surface is in the plane spanned
by the inner most detector rotation (rotation around primary beam
is ignored) and perpendicular to the primary beam. 'sam' means the
surface orientation is along the innermost sample circles rotation
direction (in this case this should be the azimuth motor to yield
the expected results). Default is 'det'.
Restrictions: the given direction can not be along the primary
beam. If one needs that case, let the maintainer know. Currently
this case is caught and a different axis is automatically used as
z-axis.
wl : float or str
wavelength of the x-rays in angstrom (default: 1.5406A)
en : float or str
energy of the x-rays in eV (default: 8048eV == 1.5406A ).
the en keyword overrules the wl keyword
Note:
The qconv argument does not change the Q2Ang function's
behavior. See Q2AngFit function in case you want to calculate
for arbitrary goniometers with some restrictions.
"""
utilities.check_kwargs(keyargs, self._valid_init_kwargs,
self.__class__.__name__)
if isinstance(ipdir, (list, tuple, numpy.ndarray)):
self.idir = math.VecUnit(ipdir)
else:
raise TypeError("Inplane direction must be list or numpy array")
if isinstance(ndir, (list, tuple, numpy.ndarray)):
self.ndir = math.VecUnit(ndir)
else:
raise TypeError("normal direction must be list or numpy array")
# test the given direction to be not parallel and warn if not
# perpendicular
if numpy.isclose(norm(numpy.cross(self.idir, self.ndir)), 0):
raise InputError("given inplane direction is parallel to normal "
"direction, they must be linear independent!")
if not numpy.isclose(numpy.abs(numpy.dot(self.idir, self.ndir)), 0):
self.idir = numpy.cross(
numpy.cross(self.ndir, self.idir),
self.ndir)
self.idir = self.idir / norm(self.idir)
warnings.warn("Experiment: given inplane direction is not "
"perpendicular to normal direction\n -> Experiment "
"class uses the following direction with the same "
"azimuth:\n %s" % (' '.join(map(
str, numpy.round(self.idir, 3)))))
# initialize Ang2Q conversion
self._A2QConversion = keyargs.get(
'qconv', QConversion('x+', 'x+', [0, 1, 0]))
self.Ang2Q = self._A2QConversion
self._sampleor = keyargs.get('sampleor', 'det')
# set the coordinate transform for the azimuth used in the experiment
self.scatplane = math.VecUnit(numpy.cross(self.idir, self.ndir))
self._set_transform(self.scatplane, self.idir,
self.ndir, self._sampleor)
# calculate the energy from the wavelength
self._set_wavelength(keyargs.get('wl', config.WAVELENGTH))
if "en" in keyargs:
self._set_energy(keyargs["en"])
def __str__(self):
ostr = "scattering plane normal: (%f %f %f)\n" % (self.scatplane[0],
self.scatplane[1],
self.scatplane[2])
ostr += "inplane azimuth: (%f %f %f)\n" % (self.idir[0],
self.idir[1],
self.idir[2])
ostr += "second refercence direction: (%f %f %f)\n" % (self.ndir[0],
self.ndir[1],
self.ndir[2])
ostr += f"energy: {self._en:f} (eV)\n"
ostr += f"wavelength: {self._wl:f} (angstrom)\n"
ostr += self._A2QConversion.__str__()
return ostr
def _set_transform(self, v1, v2, v3, sampleor='det'):
"""
set new transformation of the coordinate system to use in the
experimental class.
The sampleor variable determines the sample surface orientation with
respect to the coordinate system in which the goniometer rotations are
given. You can use the [xyz][+-] syntax to specify the nominal surface
orientation (when all goniometer angles are zero). In addition two
special values 'det' and 'sam' are available, which will let the code
determine the orientation from either the inner most detector or sample
rotation. 'det' means the surface is in the plane spanned by the inner
most detector rotation (rotation around primary beam is ignored) and
perpendicular to the primary beam. 'sam' means the surface orientation
is along the innermost sample circles rotation direction (in this case
this should be the azimuth motor to yield the expected results).
Default is 'det'.
Restrictions: the given direction can not be along the primary beam.
If one needs that case, let the maintainer know. Currently this case is
caught and a different axis is automatically used as z-axis.
"""
# turn idir to Y and ndir to Z
self._t1 = math.CoordinateTransform(v1, v2, v3)
if sampleor == 'det':
yi = self._A2QConversion.r_i
idc = self._A2QConversion.detectorAxis[-1]
xi = math.getVector(idc)
if numpy.isclose(norm(numpy.cross(xi, yi)), 0):
# this is the case when a detector rotation around the primary
# beam direction is installed
idc = self._A2QConversion.detectorAxis[-2]
xi = math.getVector(idc)
zi = math.VecUnit(numpy.cross(xi, yi))
elif sampleor == 'sam':
yi = self._A2QConversion.r_i
isc = self._A2QConversion.sampleAxis[-1]
zi = numpy.abs(math.getVector(isc))
if numpy.all(numpy.abs(yi) == numpy.abs(zi)):
zi = numpy.roll(zi, 1)
if config.VERBOSITY >= config.INFO_LOW:
print("XU.Experiment: Warning, sample orientation "
"convention failed. Using (%.3f %.3f %.3f) "
"as internal z-axis" % (zi[0], zi[1], zi[2]))
xi = math.VecUnit(numpy.cross(yi, zi))
else:
yi = self._A2QConversion.r_i
try:
zi = math.getVector(sampleor)
except InputError:
raise InputError('invalid value of sample orientation, use '
'either [xyz][+-] syntax or det/sam!')
if numpy.all(numpy.abs(yi) == numpy.abs(zi)):
zi = numpy.roll(zi, 1)
if config.VERBOSITY >= config.INFO_LOW:
print("XU.Experiment: Warning, sample orientation "
"convention failed. Using (%.3f %.3f %.3f) "
"as internal z-axis" % (zi[0], zi[1], zi[2]))
xi = math.VecUnit(numpy.cross(yi, zi))
# turn r_i to Y and Z defined by detector rotation plane
self._t2 = math.CoordinateTransform(xi, yi, zi)
self._transform = math.Transform(
numpy.dot(numpy.linalg.inv(self._t2.matrix), self._t1.matrix))
def _set_energy(self, energy):
self._en = utilities.energy(energy)
self._wl = utilities.en2lam(self._en)
self.k0 = numpy.pi * 2. / self._wl
self._A2QConversion.wavelength = self._wl
def _set_wavelength(self, wl):
self._wl = utilities.wavelength(wl)
self._en = utilities.lam2en(self._wl)
self.k0 = numpy.pi * 2. / self._wl
self._A2QConversion.wavelength = self._wl
def _get_energy(self):
return self._en
def _get_wavelength(self):
return self._wl
energy = property(_get_energy, _set_energy)
wavelength = property(_get_wavelength, _set_wavelength)
def _set_inplane_direction(self, dir):
self.idir = math.VecUnit(dir)
v1 = numpy.cross(self.ndir, self.idir)
self._set_transform(v1, self.idir, self.ndir, self._sampleor)
def _get_inplane_direction(self):
return self.idir
def _set_normal_direction(self, dir):
self.ndir = math.VecUnit(dir)
v1 = numpy.cross(self.ndir, self.idir)
self._set_transform(v1, self.idir, self.ndir, self._sampleor)
def _get_normal_direction(self):
return self.ndir
[docs]
def Q2Ang(self, qvec):
pass
[docs]
def Ang2HKL(self, *args, **kwargs):
"""
angular to (h, k, l) space conversion.
It will set the UB argument to Ang2Q and pass all other parameters
unchanged. See Ang2Q for description of the rest of the arguments.
Parameters
----------
args : list
arguments forwarded to Ang2Q
kwargs : dict, optional
optional keyword arguments
B : array-like, optional
reciprocal space conversion matrix of a Crystal. You can specify
the matrix B (default identiy matrix) shape needs to be (3, 3)
mat : Crystal, optional
Crystal object to use to obtain a B matrix (e.g. xu.materials.Si)
can be used as alternative to the B keyword argument B is favored
in case both are given
U : array-like, optional
orientation matrix U can be given. If none is given the orientation
defined in the Experiment class is used.
dettype : {'point', 'linear', 'area'}, optional
detector type: decides which routine of Ang2Q to call. default
'point'
delta : ndarray, list or tuple, optional
giving delta angles to correct the given ones for misalignment.
delta must be an numpy array or list of length 2. used angles are
than ``(om, tt) - delta``
wl : float or str, optional
x-ray wavelength in angstrom (default: self._wl)
en : float or str, optional
x-ray energy in eV (default: converted self._wl)
deg : bool, optional
flag to tell if angles are passed as degree (default: True)
sampledis : tuple or list or array-like
sample displacement vector in relative units of the detector
distance. Applies to parallel beam geometry. (default: (0, 0, 0))
Returns
-------
ndarray
H K L coordinates as numpy.ndarray with shape `(N , 3)` where `N`
corresponds to the number of points given in the input (args)
"""
valid_kwargs = {'B': 'orthonormalization matrix',
'U': 'orientation matrix',
'mat': 'material object',
'dettype': 'string with detector type'}
valid_kwargs.update(QConversion._valid_call_kwargs)
del valid_kwargs['UB']
utilities.check_kwargs(kwargs, valid_kwargs, 'Ang2HKL')
if "B" in kwargs:
B = numpy.array(kwargs['B'])
kwargs.pop("B")
elif "mat" in kwargs:
mat = kwargs['mat']
B = mat.B
kwargs.pop("mat")
else:
B = numpy.identity(3)
if "U" in kwargs:
U = numpy.array(kwargs['U'])
kwargs.pop("U")
else:
U = self._transform.matrix
kwargs['UB'] = numpy.dot(U, B)
if "dettype" in kwargs:
typ = kwargs['dettype']
if typ not in ('point', 'linear', 'area'):
raise InputError("wrong dettype given: needs to be one of "
"'point', 'linear', 'area'")
kwargs.pop("dettype")
else:
typ = 'point'
if typ == 'linear':
return self.Ang2Q.linear(*args, **kwargs)
if typ == 'area':
return self.Ang2Q.area(*args, **kwargs)
return self.Ang2Q(*args, **kwargs)
[docs]
def TiltAngle(self, q, deg=True):
"""
TiltAngle(q, deg=True):
Return the angle between a q-space position and the surface normal.
Parameters
----------
q : list or numpy array with the reciprocal space position
optional keyword arguments:
deg : True/False whether the return value should be in degree or
radians (default: True)
"""
if isinstance(q, list):
qt = numpy.array(q, dtype=numpy.double)
elif isinstance(q, numpy.ndarray):
qt = q
else:
raise TypeError("q-space position must be list or numpy array")
return math.VecAngle(self.ndir, qt, deg)
def _prepare_qvec(self, Q):
"""
check and reshape input to have the same q array for all possible types
of input
"""
if len(Q) < 3:
Q = Q[0]
if len(Q) < 3:
raise InputError("need 3 q-space vector components")
if isinstance(Q, (list, tuple, numpy.ndarray)):
q = numpy.asarray(Q, dtype=numpy.double)
else:
raise TypeError("Q vector must be a list, tuple or numpy array")
if len(q.shape) != 2:
q = q.reshape(3, -1)
return q.T
[docs]
class HXRD(Experiment):
"""
class describing high angle x-ray diffraction experiments
the class helps with calculating the angles of Bragg reflections
as well as helps with analyzing measured data
the class describes a two circle (omega, twotheta) goniometer to
help with coplanar x-ray diffraction experiments. Nevertheless 3D data
can be treated with the use of linear and area detectors.
see "help(HXRDInstance.Ang2Q)"
"""
[docs]
def __init__(self, idir, ndir, geometry='hi_lo', **keyargs):
"""
initialization routine for the HXRD Experiment class
Parameters
----------
idir, ndir, keyargs :
same as for the Experiment base class -> please look at the
docstring of Experiment.__init__ for more details
geometry : {'hi_lo', 'lo_hi', 'real'}, optional
determines the scattering geometry :
- 'hi_lo' (default) high incidence-low exit
- 'lo_hi' low incidence - high exit
- 'real' general geometry - q-coordinates determine
high or low incidence
"""
if "qconv" not in keyargs:
keyargs['qconv'] = QConversion('x+', 'x+', [0, 1, 0])
if geometry in ["hi_lo", "lo_hi", "real"]:
self.geometry = geometry
else:
raise InputError("HXRD: invalid value for the geometry "
"argument given")
Experiment.__init__(self, idir, ndir, **keyargs)
if config.VERBOSITY >= config.DEBUG:
print(
"XU.HXRD.__init__: \nEnergy: %s \nGeometry: %s \n%s---" %
(self._en, self.geometry, str(
self.Ang2Q)))
# pylint: disable-next=method-hidden
[docs]
def Ang2Q(self, om, tt, **kwargs):
"""
angular to momentum space conversion for a point detector. Also see
help HXRD.Ang2Q for procedures which treat line and area detectors
Parameters
----------
om, tt : float or array-like
sample and detector angles as numpy array, lists or Scalars must be
given. All arguments must have the same shape or length. However,
if one angle is always the same its enough to give one scalar
value.
kwargs : dict, optional
optional keyword arguments
delta : list or array-like
giving delta angles to correct the given ones for misalignment.
delta must be an numpy array or list of length 2. Used angles are
than om, tt - delta
UB : array-like
matrix for conversion from (hkl) coordinates to Q of sample used to
determine not Q but (hkl) (default: identity matrix)
wl : float or str, optional
x-ray wavelength in angstrom (default: self._wl)
deg : bool, optional
flag to tell if angles are passed as degree (default: True)
Returns
-------
ndarray
reciprocal space positions as numpy.ndarray with shape `(N , 3)`
where `N` corresponds to the number of points given in the input
"""
# dummy function to have some documentation string available
# the real function is generated dynamically in the __init__ routine
[docs]
def Q2Ang(self, *Q, **keyargs):
"""
Convert a reciprocal space vector Q to COPLANAR scattering angles. The
keyword argument trans determines whether Q should be transformed to
the experimental coordinate frame or not. The coplanar scattering
angles correspond to a goniometer with sample rotations
['x+', 'y+', 'z-'] and detector rotation 'x+' and primary beam along y.
This is a standard four circle diffractometer.
Note:
The behavior of this function is unchanged if the goniometer
definition is changed!
Parameters
----------
Q : list, tuple or array-like
array of shape (3) with q-space vector components or 3
separate lists with qx, qy, qz
trans : bool, optional
apply coordinate transformation on Q (default True)
deg : book, optional
(default True) determines if the angles are returned in radians or
degrees
geometry : {'hi_lo', 'lo_hi', 'real', 'realTilt'}, optional
determines the scattering geometry (default: self.geometry):
- 'hi_lo' high incidence and low exit
- 'lo_hi' low incidence and high exit
- 'real' general geometry with angles determined by
q-coordinates (azimuth); this and upper geometries
return [omega, 0, phi, twotheta]
- 'realTilt' general geometry with angles determined by
q-coordinates (tilt); returns [omega, chi, phi, twotheta]
refrac : bool, optional
determines if refraction is taken into account;
if True then also a material must be given (default: False)
mat : Crystal
Crystal object; needed to obtain its optical properties for
refraction correction, otherwise not used
full_output : bool, optional
determines if additional output is given to determine scattering
angles more accurately in case refraction is set to True. default:
False
fi, fd : tuple or list
if refraction correction is applied one can optionally specify the
facet through which the beam enters (fi) and exits (fd) fi, fd must
be the surface normal vectors (not transformed & not necessarily
normalized). If omitted the normal direction of the experiment is
used.
Returns
-------
ndarray
**full_output=False**: a numpy array of shape (4) with four
scattering angles which are [omega, chi, phi, twotheta];
- omega : incidence angle with respect to surface
- chi : sample tilt for the case of non-coplanar geometry
- phi : sample azimuth with respect to inplane reference
direction
- twotheta : scattering angle/detector angle
**full_output=True**: a numpy array of shape (6) with five angles
which are [omega, chi, phi, twotheta, psi_i, psi_d]
- psi_i : offset of the incidence beam from the scattering plane
due to refraction
- pdi_d : offset ot the diffracted beam from the scattering plane
due to refraction
"""
valid_kwargs = {'trans': 'flag, perform coordinate transformation',
'deg': 'flag, return degrees',
'geometry': 'geometry string',
'refrac': 'flag',
'mat': 'Crystal instance',
'fi': 'incidence facet',
'fd': 'exit facet',
'full_output': 'see docstring for details'}
utilities.check_kwargs(keyargs, valid_kwargs, 'Q2Ang')
q = self._prepare_qvec(Q)
# parse keyword arguments
geom = keyargs.get('geometry', self.geometry)
trans = keyargs.get('trans', True)
deg = keyargs.get('deg', True)
mat = keyargs.get('mat', None) # material for optical properties
refrac = keyargs.get('refrac', False)
if refrac and mat is None: # check if material is available
raise InputError("keyword argument 'mat' must be set when "
"'refrac' is set to True!")
foutp = keyargs.get('full_output', False)
fi = keyargs.get('fi', self.ndir) # incidence facet
fi = math.VecUnit(self.Transform(fi))
fd = keyargs.get('fd', self.ndir) # exit facet
fd = math.VecUnit(self.Transform(fd))
# set parameters for the calculation
z = self.Transform(self.ndir) # z
y = self.Transform(self.idir) # y
x = self.Transform(self.scatplane) # x
if refrac:
n = numpy.real(
mat.idx_refraction(
self.energy)) # index of refraction
k = self.k0 * n
else:
k = self.k0
# start calculation for each given Q-point
if foutp:
angle = numpy.zeros((6, q.shape[0]))
else:
angle = numpy.zeros((4, q.shape[0]))
if trans:
q = self.Transform(q)
if config.VERBOSITY >= config.DEBUG:
print(f"XU.HXRD.Q2Ang: q= {repr(q)}")
qa = math.VecNorm(q)
tth = 2. * numpy.arcsin(qa / 2. / k)
# calculation of the sample azimuth phi (scattering plane
# spanned by qvec[1] and qvec[2] directions)
chi = -numpy.arctan2(math.VecDot(q, x), math.VecDot(q, z))
if numpy.any(numpy.isclose(numpy.abs(math.VecDot(q, z)), 0)):
if config.VERBOSITY >= config.INFO_LOW:
print("XU.HXRD: some position is perpendicular to ndir-"
"reference direction (might be inplane or "
"unreachable)")
if geom == 'hi_lo':
# +: high incidence geometry
om = tth / 2. + math.VecAngle(q, z)
phi = -numpy.arctan2(math.VecDot(q, x), math.VecDot(q, y))
elif geom == 'lo_hi':
# -: low incidence geometry
om = tth / 2. - math.VecAngle(q, z)
phi = -numpy.arctan2(-1 * math.VecDot(q, x),
-1 * math.VecDot(q, y))
elif geom == 'real':
phi = -numpy.arctan2(math.VecDot(q, x), math.VecDot(q, y))
sign = numpy.ones(q.shape[0])
m = numpy.abs(phi) > numpy.pi / 2.
phi = -numpy.arctan2(math.VecDot(q, x), math.VecDot(q, y))
sign[m] = -1
phi[m] = -numpy.arctan2(-1 * math.VecDot(q[m], x),
-1 * math.VecDot(q[m], y))
om = tth / 2 + sign * math.VecAngle(q, z)
elif geom == 'realTilt':
phi = 0.
om = tth / 2 + numpy.arctan2(
math.VecDot(q, y),
numpy.sqrt(math.VecDot(q, z) ** 2 + math.VecDot(q, x) ** 2))
else:
raise ValueError("HXRD: invalid value for the geometry argument "
"given\n valid entries are: hi_lo, lo_hi, real, "
"realTilt")
# refraction correction at incidence and exit facet
psi_i = numpy.zeros_like(tth)
psi_d = numpy.zeros_like(tth) # if refrac is false and full_output
if refrac:
beta = tth - om
ki = k * (numpy.cos(om)[:, numpy.newaxis] * y[numpy.newaxis, :] -
numpy.sin(om)[:, numpy.newaxis] * z[numpy.newaxis, :])
kd = k * (numpy.cos(beta)[:, numpy.newaxis] * y[numpy.newaxis, :] +
numpy.sin(beta)[:, numpy.newaxis] * z[numpy.newaxis, :])
# refraction at incidence facet
m = math.VecDot(ki, fi) > 0
if numpy.any(m):
print("XU.HXRD: Warning, incidence facet not hit by "
"primary beam for all positions! check your input!")
om[m] = numpy.nan
tth[m] = numpy.nan
mnot = numpy.logical_not(m)
cosbi = numpy.abs(math.VecDot(ki, fi) / math.VecNorm(ki))
cosb0 = numpy.sqrt(1 - n ** 2 * (1 - cosbi ** 2))
ki0 = self.k0 * (n * math.VecUnit(ki) -
(numpy.sign(math.VecDot(ki, fi)) *
(n * cosbi - cosb0))[:, numpy.newaxis] *
fi[numpy.newaxis, :])
om[mnot] = math.VecAngle(ki0, y)
psi_i[mnot] = numpy.arcsin(math.VecDot(ki0, x) / self.k0)
if config.VERBOSITY >= config.DEBUG:
print(f"XU.HXRD.Q2Ang: ki, ki0 = {repr(ki)} {repr(ki0)}")
# refraction at exit facet
m = math.VecDot(kd, fd) < 0
if numpy.any(m):
print("XU.HXRD: Warning, exit facet not hit by "
"diffracted beam! check your input!")
om[m] = numpy.nan
tth[m] = numpy.nan
cosbd = numpy.abs(math.VecDot(kd, fd) / math.VecNorm(kd))
cosb0 = numpy.sqrt(1 - n ** 2 * (1 - cosbd ** 2))
kd0 = self.k0 * (n * math.VecUnit(kd) -
(numpy.sign(math.VecDot(kd, fd)) *
(n * cosbd - cosb0))[:, numpy.newaxis] *
fd[numpy.newaxis, :])
tth[mnot] = math.VecAngle(ki0, kd0)
psi_d[mnot] = numpy.arcsin(numpy.dot(kd0, x) / self.k0)
if config.VERBOSITY >= config.DEBUG:
print(f"XU.HXRD.Q2Ang: kd, kd0 = {repr(kd)} {repr(kd0)}")
if geom == 'realTilt':
angle[0, :] = om
angle[1, :] = chi
angle[3, :] = tth
else:
angle[0, :] = om
angle[2, :] = phi
angle[3, :] = tth
if foutp:
angle[4, :] = psi_i
angle[5, :] = psi_d
if q.shape[0] == 1:
angle = angle.flatten()
if config.VERBOSITY >= config.INFO_ALL:
print("XU.HXRD.Q2Ang: om, chi, phi, tth,[psi_i, psi_d] = %s"
% repr(angle))
if deg:
return numpy.degrees(angle)
return angle
[docs]
class FourC(HXRD):
"""
class describing high angle x-ray diffraction experiments
the class helps with calculating the angles of Bragg reflections
as well as helps with analyzing measured data
the class describes a four circle (omega, chi, phi, twotheta) goniometer to
help with coplanar x-ray diffraction experiments. Nevertheless 3D data can
be treated with the use of linear and area detectors. see
"help(FourCInstance.Ang2Q)"
"""
[docs]
def __init__(self, idir, ndir, **keyargs):
"""
initialization routine for the FourC Experiment class
Parameters
----------
idir, ndir, keyargs :
same as for the Experiment base class -> please look at the
docstring of Experiment.__init__ for more details
geometry : {'hi_lo', 'lo_hi', 'real'}, optional
determines the scattering geometry :
- 'hi_lo' (default) high incidence-low exit
- 'lo_hi' low incidence - high exit
- 'real' general geometry - q-coordinates determine
high or low incidence
"""
if "qconv" not in keyargs:
# 3S+1D goniometer (standard four-circle goniometer,
# omega, chi, phi, theta)
keyargs['qconv'] = QConversion(['x+', 'y+', 'z-'],
'x+', [0, 1, 0])
HXRD.__init__(self, idir, ndir, **keyargs)
[docs]
class NonCOP(Experiment):
"""
class describing high angle x-ray diffraction experiments. The class helps
with calculating the angles of Bragg reflections as well as helps with
analyzing measured data for NON-COPLANAR measurements, where the tilt is
used to align asymmetric peaks, like in the case of a polefigure
measurement.
The class describes a four circle (omega, chi, phi, twotheta) goniometer to
help with x-ray diffraction experiments. Linear and area detectors can be
treated as described in "help(NonCOPInstance.Ang2Q)"
"""
[docs]
def __init__(self, idir, ndir, **keyargs):
"""
initialization routine for the NonCOP Experiment class
Parameters
----------
idir, ndir, keyargs :
same as for the Experiment base class
"""
if "qconv" not in keyargs:
# 3S+1D goniometer (standard four-circle goniometer,
# omega, chi, phi, theta)
keyargs['qconv'] = QConversion(['x+', 'y+', 'z-'],
'x+', [0, 1, 0])
Experiment.__init__(self, idir, ndir, **keyargs)
# pylint: disable-next=method-hidden
[docs]
def Ang2Q(self, om, chi, phi, tt, **kwargs):
"""
angular to momentum space conversion for a point detector. Also see
help NonCOP.Ang2Q for procedures which treat line and area detectors
Parameters
----------
om, chi, phi, tt : float or array-like
sample and detector angles as numpy array, lists or Scalars must be
given. All arguments must have the same shape or length. However,
if one angle is always the same its enough to give one scalar
value.
kwargs : dict, optional
optional keyword arguments
delta : list, tuple or array-like, optional
giving delta angles to correct the given ones for misalignment
delta must be an numpy array or list of length 4. Used angles are
than om, chi, phi, tt - delta
UB : array-like, optional
matrix for conversion from (hkl) coordinates to Q of sample used to
determine not Q but (hkl) (default: identity matrix)
wl : float or str, optional
x-ray wavelength in angstrom (default: self._wl)
deg : bool, optional
flag to tell if angles are passed as degree (default: True)
Returns
-------
ndarray
reciprocal space positions as numpy.ndarray with shape `(N , 3)`
where `N` corresponds to the number of points given in the input
"""
# dummy function to have some documentation string available
# the real function is generated dynamically in the __init__ routine
[docs]
def Q2Ang(self, *Q, **keyargs):
"""
Convert a reciprocal space vector Q to NON-COPLANAR scattering angles.
The keyword argument trans determines whether Q should be transformed
to the experimental coordinate frame or not.
Note:
The behavior of this function is unchanged if the goniometer
definition is changed!
Parameters
----------
Q : list, tuple or array-like
array of shape (3) with q-space vector components or 3
separate lists with qx, qy, qz
trans : bool, optional
apply coordinate transformation on Q (default True)
deg : book, optional
(default True) determines if the angles are returned in radians or
degrees
Returns
-------
ndarray
a numpy array of shape (4) with four scattering
angles which are [omega, chi, phi, twotheta];
- omega : incidence angle with respect to surface
- chi : sample tilt for the case of non-coplanar geometry
- phi : sample azimuth with respect to inplane reference
direction
- twotheta : scattering angle/detector angle
"""
valid_kwargs = {'trans': 'coordinate transformation flag',
'deg': 'degree-flag'}
utilities.check_kwargs(keyargs, valid_kwargs, 'Q2Ang')
q = self._prepare_qvec(Q)
trans = keyargs.get('trans', True)
deg = keyargs.get('deg', True)
angle = numpy.zeros((4, q.shape[0]))
# set parameters for the calculation
z = self.Transform(self.ndir) # z
y = self.Transform(self.idir) # y
x = self.Transform(self.scatplane) # x
if trans:
q = self.Transform(q)
if config.VERBOSITY >= config.DEBUG:
print(f"XU.NonCOP.Q2Ang: q= {repr(q)}")
qa = math.VecNorm(q)
tth = 2. * numpy.arcsin(qa / 2. / self.k0)
om = tth / 2.
# calculation of the sample azimuth
# the sign depends on the phi movement direction
phi = -1 * numpy.arctan2(
math.VecDot(q, x),
math.VecDot(q, y)) - numpy.pi / 2.
chi = (math.VecAngle(q, z))
angle[0, :] = om
angle[1, :] = chi
angle[2, :] = phi
angle[3, :] = tth
if q.shape[0] == 1:
angle = angle.flatten()
if config.VERBOSITY >= config.INFO_ALL:
print(f"XU.HXRD.Q2Ang: [om, chi, phi, tth] = {repr(angle)}")
if deg:
return numpy.degrees(angle)
return angle
[docs]
class GID(Experiment):
"""
class describing grazing incidence x-ray diffraction experiments
the class helps with calculating the angles of Bragg reflections
as well as it helps with analyzing measured data
the class describes a four circle (alpha_i, azimuth, twotheta, beta)
goniometer to help with GID experiments at the ROTATING ANODE.
3D data can be treated with the use of linear and area detectors.
see "help(GIDInstance.Ang2Q)"
Using this class the default sample surface orientation is determined by
the inner most sample rotation (which is usually the azimuth motor).
"""
[docs]
def __init__(self, idir, ndir, **keyargs):
"""
initialization routine for the GID Experiment class
- ``idir`` defines the inplane reference direction (idir points into
the PB direction at zero angles)
- ``ndir`` defines the surface normal of your sample (ndir points
along the innermost sample rotation axis)
Parameters
----------
idir, ndir, keyargs :
same as for the Experiment base class
"""
if 'sampleor' not in keyargs:
keyargs['sampleor'] = 'sam'
if "qconv" not in keyargs:
# 2S+2D goniometer
keyargs['qconv'] = QConversion(['z-', 'x+'], ['x+', 'z-'],
[0, 1, 0])
Experiment.__init__(self, idir, ndir, **keyargs)
[docs]
def Q2Ang(self, qvec, trans=True, deg=True, **kwargs):
"""
calculate the GID angles needed in the experiment
the inplane reference direction defines the direction were
the reference direction is parallel to the primary beam
(i.e. lattice planes perpendicular to the beam)
Note:
The behavior of this function is unchanged if the goniometer
definition is changed!
Parameters
----------
qvec : list, tuple or array-like
array of shape (3) with q-space vector components or 3
separate lists with qx, qy, qz
trans : bool, optional
apply coordinate transformation on Q (default True)
deg : book, optional
(default True) determines if the angles are returned in radians or
degrees
Returns
-------
ndarray
a numpy array of shape (4) with four GID scattering
angles which are [alpha_i, azimuth, twotheta, beta];
- alpha_i : incidence angle to surface (at the moment always 0)
- azimuth : sample rotation with respect to the inplane
reference direction
- twotheta : scattering angle
- beta : exit angle from surface (at the moment always 0)
"""
valid_kwargs = {'trans': 'coordinate transformation flag',
'deg': 'degree-flag'}
utilities.check_kwargs(kwargs, valid_kwargs, 'Q2Ang')
if isinstance(qvec, list):
q = numpy.array(qvec, dtype=numpy.double)
elif isinstance(qvec, numpy.ndarray):
q = qvec
else:
raise TypeError("Q vector must be a list or numpy array")
if trans:
q = self.Transform(q)
if config.VERBOSITY >= config.INFO_ALL:
print(f"XU.GID.Q2Ang: q = {repr(q)}")
# set parameters for the calculation
z = self.Transform(self.ndir) # z
y = self.Transform(self.idir) # y
x = self.Transform(self.scatplane) # x
# check if reflection is inplane
if numpy.abs(math.VecDot(q, z)) >= 0.001:
raise InputError(
f"Reflection not reachable in GID geometry (Q: {str(q)})")
# calculate angle to inplane reference direction
aref = numpy.arctan2(math.VecDot(q, x), math.VecDot(q, y))
# calculate scattering angle
qa = math.VecNorm(q)
tth = 2. * numpy.arcsin(qa / 2. / self.k0)
azimuth = numpy.pi / 2 + aref + tth / 2.
if deg:
ang = [0, numpy.degrees(azimuth), numpy.degrees(tth), 0]
else:
ang = [0, azimuth, tth, 0]
if config.VERBOSITY >= config.INFO_ALL:
print(f"XU.GID.Q2Ang: [ai, azimuth, tth, beta] = {str(ang)}\n"
f"difference to inplane reference which is {aref:5.2f}")
return ang
# pylint: disable-next=method-hidden
[docs]
def Ang2Q(self, ai, phi, tt, beta, **kwargs):
"""
angular to momentum space conversion for a point detector. Also see
help GID.Ang2Q for procedures which treat line and area detectors
Parameters
----------
ai, phi, tt, beta : float or array-like
sample and detector angles as numpy array, lists or Scalars must be
given. All arguments must have the same shape or length. However,
if one angle is always the same its enough to give one scalar
value.
kwargs : dict, optional
optional keyword arguments
delta : list, tuple or array-like, optional
giving delta angles to correct the given ones for misalignment
delta must be an numpy array or list of length 4. Used angles are
then ``ai, phi, tt, beta - delta``
UB : array-like, optional
matrix for conversion from (hkl) coordinates to Q of sample used to
determine not Q but (hkl) (default: identity matrix)
wl : float or str, optional
x-ray wavelength in angstrom (default: self._wl)
deg : bool, optional
flag to tell if angles are passed as degree (default: True)
Returns
-------
ndarray
reciprocal space positions as numpy.ndarray with shape `(N , 3)`
where `N` corresponds to the number of points given in the input
"""
# dummy function to have some documentation string available
# the real function is generated dynamically in the __init__ routine
[docs]
class GISAXS(Experiment):
"""
class describing grazing incidence x-ray diffraction experiments
the class helps with calculating the angles of Bragg reflections
as well as it helps with analyzing measured data
the class describes a three circle (alpha_i, twotheta, beta)
goniometer to help with GISAXS experiments at the ROTATING ANODE.
3D data can be treated with the use of linear and area detectors.
see help self.Ang2Q
"""
[docs]
def __init__(self, idir, ndir, **keyargs):
"""
initialization routine for the GISAXS Experiment class
``idir`` defines the inplane reference direction (idir points into the
PB direction at zero angles)
Parameters
----------
idir, ndir, keyargs :
same as for the Experiment base class
"""
if "qconv" not in keyargs:
# 1S+2D goniometer
keyargs['qconv'] = QConversion(['x+'], ['x+', 'z-'],
[0, 1, 0])
Experiment.__init__(self, idir, ndir, **keyargs)
[docs]
def Q2Ang(self, Q, trans=True, deg=True, **kwargs):
pass
# pylint: disable-next=method-hidden
[docs]
def Ang2Q(self, ai, tt, beta, **kwargs):
"""
angular to momentum space conversion for a point detector. Also see
help GISAXS.Ang2Q for procedures which treat line and area detectors
Parameters
----------
ai, tt, beta : float or array-like
sample and detector angles as numpy array, lists or Scalars must be
given. all arguments must have the same shape or length. Howevver,
if one angle is always the same its enough to give one scalar
value.
kwargs : dict, optional
optional keyword arguments
delta : list, tuple or array-like, optional
giving delta angles to correct the given ones for misalignment
delta must be an numpy array or list of length 3. Used angles are
then ``ai, tt, beta - delta``
UB : array-like, optional
matrix for conversion from (hkl) coordinates to Q of sample used to
determine not Q but (hkl) (default: identity matrix)
wl : float or str, optional
x-ray wavelength in angstrom (default: self._wl)
deg : bool, optional
flag to tell if angles are passed as degree (default: True)
Returns
-------
ndarray
reciprocal space positions as numpy.ndarray with shape `(N , 3)`
where `N` corresponds to the number of points given in the input
"""
# dummy function to have some documentation string available
# the real function is generated dynamically in the __init__ routine
[docs]
class PowderExperiment(Experiment):
"""
Experimental class for powder diffraction which helps to convert theta
angles to momentum transfer space
"""
[docs]
def __init__(self, **kwargs):
"""
class constructor which takes the same keyword arguments as the
Experiment class
Parameters
----------
kwargs : dict, optional
keyword arguments same as for the Experiment base class
"""
Experiment.__init__(self, [0, 1, 0], [0, 0, 1], **kwargs)
self.Ang2Q = self._Ang2Q
def _Ang2Q(self, th, wl=None, deg=True):
"""
Converts theta angles to reciprocal space positions
returns the absolute value of momentum transfer
"""
if deg:
lth = numpy.radians(th)
else:
lth = th
if wl:
k0 = 2 * numpy.pi / wl
else:
k0 = self.k0
qpos = 2 * k0 * numpy.sin(lth)
return qpos
[docs]
def Q2Ang(self, qpos, wl=None, deg=True):
"""
Converts reciprocal space values to theta angles
"""
if wl:
k0 = 2 * numpy.pi / wl
else:
k0 = self.k0
th = numpy.arcsin(numpy.divide(qpos, (2 * k0)))
if deg:
th = numpy.degrees(th)
return th