# 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 Eugen Wintersberger <eugen.wintersberger@desy.de>
# Copyright (c) 2009-2023 Dominik Kriegner <dominik.kriegner@gmail.com>
import math
import re
import numpy
from .. import config
from ..exception import InputError
circleSyntax = re.compile("[xyzk][+-]")
def _checkvec(v):
if isinstance(v, (list, tuple, numpy.ndarray)):
vtmp = numpy.asarray(v, dtype=numpy.double)
else:
raise TypeError("Vector must be a list, tuple or numpy array")
return vtmp
[docs]
def VecNorm(v):
"""
Calculate the norm of a vector.
Parameters
----------
v : list or array-like
input vector(s), either one vector or an array of vectors with shape
(n, 3)
Returns
-------
float or ndarray
vector norm, either a single float or shape (n, )
"""
if isinstance(v, numpy.ndarray):
if len(v.shape) >= 2:
return numpy.linalg.norm(v, axis=-1)
if len(v) != 3:
raise ValueError(
f"Vector must be of length 3, but has length {len(v)}!")
return math.sqrt(v[0]**2 + v[1]**2 + v[2]**2)
[docs]
def VecUnit(v):
"""
Calculate the unit vector of v.
Parameters
----------
v : list or array-like
input vector(s), either one vector or an array of vectors with shape
(n, 3)
Returns
-------
ndarray
unit vector of `v`, either shape (3, ) or (n, 3)
"""
vtmp = _checkvec(v)
if len(vtmp.shape) == 1:
return vtmp / VecNorm(vtmp)
return vtmp / VecNorm(vtmp)[..., numpy.newaxis]
[docs]
def VecDot(v1, v2):
"""
Calculate the vector dot product.
Parameters
----------
v1, v2 : list or array-like
input vector(s), either one vector or an array of vectors with shape
(n, 3)
Returns
-------
float or ndarray
innter product of the vectors, either a single float or (n, )
"""
if isinstance(v1, numpy.ndarray):
if len(v1.shape) >= 2:
return numpy.einsum('...i, ...i', v1, v2)
if len(v1) != 3 or len(v2) != 3:
raise ValueError("Vectors must be of size 3! (len(v1)=%d len(v2)=%d)"
% (len(v1), len(v2)))
return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]
[docs]
def VecCross(v1, v2, out=None):
"""
Calculate the vector cross product.
Parameters
----------
v1, v2 : list or array-like
input vector(s), either one vector or an array of vectors with shape
(n, 3)
out : list or array-like, optional
output vector
Returns
-------
ndarray
cross product either of shape (3, ) or (n, 3)
"""
if isinstance(v1, numpy.ndarray):
if len(v1.shape) >= 2 or len(v2.shape) >= 2:
return numpy.cross(v1, v2)
if len(v1) != 3 or len(v2) != 3:
raise ValueError("Vectors must be of size 3! (len(v1)=%d len(v2)=%d)"
% (len(v1), len(v2)))
if out is None:
out = numpy.empty(3)
out[0] = v1[1] * v2[2] - v1[2] * v2[1]
out[1] = v1[2] * v2[0] - v1[0] * v2[2]
out[2] = v1[0] * v2[1] - v1[1] * v2[0]
return out
[docs]
def VecAngle(v1, v2, deg=False):
"""
calculate the angle between two vectors. The following
formula is used
v1.v2 = norm(v1)*norm(v2)*cos(alpha)
alpha = acos((v1.v2)/(norm(v1)*norm(v2)))
Parameters
----------
v1, v2 : list or array-like
input vector(s), either one vector or an array of vectors with shape
(n, 3)
deg: bool, optional
True: return result in degree, False: in radiants (default: False)
Returns
-------
float or ndarray
the angle included by the two vectors `v1` and `v2`, either a single
float or an array with shape (n, )
"""
u1 = VecNorm(v1)
u2 = VecNorm(v2)
if isinstance(u1, numpy.ndarray) or isinstance(u2, numpy.ndarray):
s = VecDot(v1, v2) / u1 / u2
s[numpy.abs(s) > 1.0] = numpy.sign(s[numpy.abs(s) > 1.0]) * 1.0
alpha = numpy.arccos(s)
if deg:
alpha = numpy.degrees(alpha)
else:
alpha = math.acos(max(min(1., VecDot(v1, v2) / u1 / u2), -1.))
if deg:
alpha = math.degrees(alpha)
return alpha
[docs]
def distance(x, y, z, point, vec):
"""
calculate the distance between the point (x, y, z) and the line defined by
the point and vector vec
Parameters
----------
x : float or ndarray
x coordinate(s) of the point(s)
y : float or ndarray
y coordinate(s) of the point(s)
z : float or ndarray
z coordinate(s) of the point(s)
point : tuple, list or ndarray
3D point on the line to which the distance should be calculated
vec : tuple, list or ndarray
3D vector defining the propergation direction of the line
"""
coords = numpy.vstack((numpy.ravel(x) - point[0],
numpy.ravel(y) - point[1],
numpy.ravel(z) - point[2])).T
ret = VecNorm(VecCross(coords, numpy.asarray(vec)))/VecNorm(vec)
if isinstance(x, numpy.ndarray):
ret = ret.reshape(x.shape)
else:
ret = ret[0]
return ret
[docs]
def getVector(string):
"""
returns unit vector along a rotation axis given in the syntax
'x+' 'z-' or equivalents
Parameters
----------
string: str
vector string following the synthax [xyz][+-]
Returns
-------
ndarray
vector along the given direction
"""
if len(string) != 2:
raise InputError("wrong length of string for conversion given")
if not circleSyntax.search(string):
raise InputError(f"getVector: incorrect string syntax ({string})")
if string[0] == 'x':
v = [1., 0, 0]
elif string[0] == 'y':
v = [0, 1., 0]
elif string[0] == 'z':
v = [0, 0, 1.]
elif string[0] == 'k':
# determine reference direction
if config.KAPPA_PLANE[0] == 'x':
v = numpy.array((1., 0, 0))
# turn reference direction
if config.KAPPA_PLANE[1] == 'y':
v = ZRotation(config.KAPPA_ANGLE)(v)
elif config.KAPPA_PLANE[1] == 'z':
v = YRotation(-1 * config.KAPPA_ANGLE)(v)
else:
raise TypeError("getVector: invalid kappa_plane in config!")
elif config.KAPPA_PLANE[0] == 'y':
v = numpy.array((0, 1., 0))
# turn reference direction
if config.KAPPA_PLANE[1] == 'z':
v = XRotation(config.KAPPA_ANGLE)(v)
elif config.KAPPA_PLANE[1] == 'x':
v = ZRotation(-1 * config.KAPPA_ANGLE)(v)
else:
raise TypeError("getVector: invalid kappa_plane in config!")
elif config.KAPPA_PLANE[0] == 'z':
v = numpy.array((0, 0, 1.))
# turn reference direction
if config.KAPPA_PLANE[1] == 'x':
v = YRotation(config.KAPPA_ANGLE)(v)
elif config.KAPPA_PLANE[1] == 'y':
v = XRotation(-1 * config.KAPPA_ANGLE)(v)
else:
raise TypeError("getVector: invalid kappa_plane in config!")
else:
raise TypeError("getVector: invalid kappa_plane in config!")
else:
raise InputError("wrong first character of string given "
"(needs to be one of x, y, z, or k)")
if string[1] == '+':
v = numpy.asarray(v) * (+1)
elif string[1] == '-':
v = numpy.asarray(v) * (-1)
else:
raise InputError("wrong second character of string given "
"(needs to be + or -)")
return v
[docs]
def getSyntax(vec):
"""
returns vector direction in the syntax
'x+' 'z-' or equivalents
therefore works only for principle vectors of the coordinate system
like e.g. [1, 0, 0] or [0, 2, 0]
Parameters
----------
vec : list or array-like
vector of length 3
Returns
-------
str
vector string following the synthax [xyz][+-]
"""
v = _checkvec(vec)
if len(v) != 3:
raise InputError("no valid 3D vector given")
x = [1, 0, 0]
y = [0, 1, 0]
z = [0, 0, 1]
if numpy.isclose(VecNorm(numpy.cross(numpy.cross(x, y), v)), 0):
if v[2] >= 0:
string = 'z+'
else:
string = 'z-'
elif numpy.isclose(VecNorm(numpy.cross(numpy.cross(x, z), v)), 0):
if v[1] >= 0:
string = 'y+'
else:
string = 'y-'
elif numpy.isclose(VecNorm(numpy.cross(numpy.cross(y, z), v)), 0):
if v[0] >= 0:
string = 'x+'
else:
string = 'x-'
else:
raise InputError("no valid 3D vector given")
return string
[docs]
class AxisToZ(CoordinateTransform):
"""
Creates a coordinate transformation to move a certain axis to the z-axis.
The rotation is done along the great circle. The x-axis of the new
coordinate frame is created to be normal to the new and original z-axis.
The new y-axis is create in order to obtain a right handed coordinate
system.
"""
[docs]
def __init__(self, newzaxis):
"""
initialize the CoordinateTransformation to move a certain axis to the
z-axis
Parameters
----------
newzaxis : list or array-like
new z-axis
"""
newz = _checkvec(newzaxis)
if numpy.isclose(VecAngle([0, 0, 1], newz), 0):
newx = [1, 0, 0]
newy = [0, 1, 0]
newz = [0, 0, 1]
elif numpy.isclose(VecAngle([0, 0, 1], -newz), 0):
newx = [-1, 0, 0]
newy = [0, 1, 0]
newz = [0, 0, -1]
else:
newx = numpy.cross(newz, [0, 0, 1])
newy = numpy.cross(newz, newx)
CoordinateTransform.__init__(self, newx, newy, newz)
[docs]
class AxisToZ_keepXY(CoordinateTransform):
"""
Creates a coordinate transformation to move a certain axis to the z-axis.
The rotation is done along the great circle. The x-axis/y-axis of the new
coordinate frame is created to be similar to the old x and y directions.
This variant of AxisToZ assumes that the new Z-axis has its main component
along the Z-direction
"""
[docs]
def __init__(self, newzaxis):
"""
initialize the CoordinateTransformation to move a certain axis to the
z-axis
Parameters
----------
newzaxis : list or array-like
new z-axis
"""
newz = _checkvec(newzaxis)
if numpy.isclose(VecAngle([0, 0, 1], newz), 0):
newx = [1, 0, 0]
newy = [0, 1, 0]
newz = [0, 0, 1]
elif numpy.isclose(VecAngle([0, 0, 1], -newz), 0):
newx = [-1, 0, 0]
newy = [0, 1, 0]
newz = [0, 0, -1]
else:
newx = numpy.cross(newz, [0, 0, 1])
newy = numpy.cross(newz, newx)
# rotate newx and newy to be similar to old directions
ang = numpy.degrees(numpy.arctan2(newz[0], newz[1]))
newx = rotarb(newx, newz, ang)
newy = rotarb(newy, newz, ang)
CoordinateTransform.__init__(self, newx, newy, newz)
def _sincos(alpha, deg):
if deg:
a = numpy.radians(alpha)
else:
a = alpha
return numpy.sin(a), numpy.cos(a)
[docs]
def XRotation(alpha, deg=True):
"""
Returns a transform that represents a rotation about the x-axis
by an angle alpha. If deg=True the angle is assumed to be in
degree, otherwise the function expects radiants.
"""
sina, cosa = _sincos(alpha, deg)
m = numpy.array(
[[1, 0, 0], [0, cosa, -sina], [0, sina, cosa]], dtype=numpy.double)
return Transform(m)
[docs]
def YRotation(alpha, deg=True):
"""
Returns a transform that represents a rotation about the y-axis
by an angle alpha. If deg=True the angle is assumed to be in
degree, otherwise the function expects radiants.
"""
sina, cosa = _sincos(alpha, deg)
m = numpy.array(
[[cosa, 0, sina], [0, 1, 0], [-sina, 0, cosa]], dtype=numpy.double)
return Transform(m)
[docs]
def ZRotation(alpha, deg=True):
"""
Returns a transform that represents a rotation about the z-axis
by an angle alpha. If deg=True the angle is assumed to be in
degree, otherwise the function expects radiants.
"""
sina, cosa = _sincos(alpha, deg)
m = numpy.array(
[[cosa, -sina, 0], [sina, cosa, 0], [0, 0, 1]], dtype=numpy.double)
return Transform(m)
# helper scripts for rotations around arbitrary axis
[docs]
def tensorprod(vec1, vec2):
"""
function implements an elementwise multiplication of two vectors
"""
return vec1[:, numpy.newaxis] * numpy.ones((3, 3)) * vec2[numpy.newaxis, :]
[docs]
def mycross(vec, mat):
"""
function implements the cross-product of a vector with each column of a
matrix
"""
out = numpy.zeros((3, 3))
for i in range(3):
out[:, i] = numpy.cross(vec, mat[:, i])
return out
[docs]
def ArbRotation(axis, alpha, deg=True):
"""
Returns a transform that represents a rotation around an arbitrary axis by
the angle alpha. positive rotation is anti-clockwise when looking from
positive end of axis vector
Parameters
----------
axis : list or array-like
rotation axis
alpha : float
rotation angle in degree (deg=True) or in rad (deg=False)
deg : bool
determines the input format of ang (default: True)
Returns
-------
Transform
"""
axis = _checkvec(axis)
e = axis / numpy.linalg.norm(axis)
if deg:
rad = numpy.radians(alpha)
else:
rad = alpha
get = tensorprod(e, e)
rot = get + numpy.cos(rad) * (numpy.identity(3) - get) + \
numpy.sin(rad) * mycross(e, numpy.identity(3))
return Transform(rot)
[docs]
def rotarb(vec, axis, ang, deg=True):
"""
function implements the rotation around an arbitrary axis by an angle ang
positive rotation is anti-clockwise when looking from positive end of axis
vector
Parameters
----------
vec : list or array-like
vector to rotate
axis : list or array-like
rotation axis
ang : float
rotation angle in degree (deg=True) or in rad (deg=False)
deg : bool
determines the input format of ang (default: True)
Returns
-------
rotvec : rotated vector as numpy.array
Examples
--------
>>> rotarb([1, 0, 0], [0, 0, 1], 90)
array([6.123234e-17, 1.000000e+00, 0.000000e+00])
"""
return ArbRotation(axis, ang, deg)(vec)