3.1. NeXus reference frames¶
Reference frames or coordinate systems are used in several NeXus base classes. Here we extend the NeXus class documentation regarding reference frames with mathematical background and examples.
3.2. NXtransformations reference frame¶
The default reference frame in Nexus is the Nexus reference frame which is the standard Euclidean basis with additional conventions for position and orientation of source, sample, detector and other objects.
Objects can be positioned in this reference frame by chaining proper rigid transformations. The NXtransformations allow for the representation of such chain: translations are representation by vectors, rotations are represented by rotation vectors (direction and angle) and directions allow defining a different reference frame than the Nexus reference frame. Together the chain of transformation defines an activate transformation matrix in homogeneous coordinates.
myentry:NXentry
instrument:NXinstrument
mydetector:NXdetector
depends_on = "diffr/gravity"
diffr:NXtransformations
gravity = float64(0)
@transformation_type=direction
@vector=0,0,-1
beam = float64(0)
@transformation_type=direction
@vector=1,0,0
@depends_on=beam
distance = float64(10)
@transformation_type=translation
@vector=1,0,0
@depends_on=beam
roll = float64(20)
@transformation_type=rotation
@vector=0,0,1
@depends_on=distance
pitch = float64(30)
@transformation_type=rotation
@vector=0,1,0
@depends_on=roll
In this example we define the reference frame having the Z-axis being vertical and pointing upwards and the X-axis pointing along the beam. Within that reference frame the detector is translated downstream along the beam and than rolled and pitched. The coodinates of a detector pixel in tge laboratory reference frame is given by
where R
and T
are activate transformation matrices for rotation and translation with respect to a specific axis.
3.3. NXsample reference frame¶
NXtransformations can be used to define a reference frame and position a sample
myentry:NXentry
instrument:NXinstrument
mydetector:NXsample
depends_on = "diffr/phi"
diffr:NXtransformations
phi = float64(0)
@transformation_type=rotation
@vector=0,1,0
@depends_on=chi
chi = float64(0)
@transformation_type=rotation
@vector=0,0,1
@depends_on=rotation_angle
rotation_angle = float64(0)
@transformation_type=rotation
@vector=0,1,0
When the sample is a single crystal, its orientation can defined by a so-called UB-matrix (the ub_matrix field of NXsample).
3.4. NXdata reference frame¶
The axis values in NXdata are the coordinates with respect the reference frame in which the data is plotted.
We will refer to the data frame as the reference frame in which the axis coordinates are provided and the plotting frame as the reference frame in which data is to be displayed.
Example in \(\mathbb{R}^3\):
myplot:NXdata
@axes = ['Ql', 'Qk', 'Qh']
@passive_transformation = "reciprocal_space"
Qh = float64(1601)
@long_name = 'H (rlu)'
Qk = float64(801)
@long_name = 'K (rlu)'
Ql = float64(1201)
@long_name = 'L (rlu)'
reciprocal_space = float64(4, 4)
@transformation_type = 'affine'
The \((n+1) \times (n+1)\) matrix is the change-of-frame matrix or passive transformation matrix. It defines a homography in homogeneous coordinates
where \(T\) is the position of the origin of the plotting frame in the data frame and the columns of \(C\) are the coordinates of the basis vectors of the plotting frame with respect to the basis of the data frame. \(P\) can be used to describe projective transformations like the projections of a sample on a flat detector in full field techniques.
The transformation type is an optional attribute (enumeration) that describes the type of transformation. This can be useful for plotting applications to check whether the provided transformation is supported.
translation
proper rigid: translation + rotations
rigid: proper rigid + reflections
similarity: rigid + isotropic scaling
affine: similarity + non-isotropic scaling and shear
linear: affine without translation
homography: affine + projection
Note that the NXdata fields offset and scaling_factor should be used to get the real axis values from the stored axis values. These two factors could in principle also be absorbed in the passive transformation matrix.
Most plotting libraries support at least up to affine transformations. This example demonstrates the plotting of an image with an affine transformation in \(\mathbb{R}^2\) using the popular python plotting library matplotlib.
Note that the change-of-basis matrix is related to the length of the basis vectors and the angles between the basis vectors. For example in \(\mathbb{R}^3\)
where \(a\), \(b\) and \(c\) then lengths and \(\alpha\), \(\beta\) and \(\gamma\) the angles.
3.5. Euclidean space¶
The mathematics of all reference frames used in NeXus takes place in Euclidian space.
Euclidian space \(\mathbb{E}^n\) is an affine space over \(\mathbb{R}^n\) with the Euclidian inner product (a.k.a. the dot product)
3.5.1. Reference frame¶
The coordinates \((a_1, \dots, a_n)\) with \(a_i\in\mathbb{R}\) of a point \(a\) are defined with respect the a reference frame \((o, \vec{b}_1, \dots, \vec{b}_n)\) where \(o\) is the origin and \(\vec{b}_i\) the basis vectors so that
The standard reference frame of Eucledian space is the right-handed orthonormal frame with the following basis
3.5.2. Change of reference frame¶
If \((a_1, \dots, a_n)\) are the coordinates with respect to reference frame \((o, \vec{b}_1, \dots, \vec{b}_n)\) and \((a_1^\prime, \dots, a_n^\prime)\) are the coordinates with respect to reference frame \((o^\prime, \vec{b}_1^\prime, \dots, \vec{b}_n^\prime)\) then those coordinates are related by
where
\(A\) are the coordinates \((a_1, \dots, a_n)\)
\(A^\prime\) are the coordinates \((a_1^\prime, \dots, a_n^\prime)\)
\(T\) are the coordinates of vector \(\vec{oo^\prime}\) in reference frame \((o, \vec{b}_1, \dots, \vec{b}_n)\)
the columns of \(C\) are the coordinates of \(\vec{b}_i^\prime\) with respect to \((\vec{b}_1, \dots, \vec{b}_n)\)
\(P\) are zeros.
We call \(T\) the translation vector, \(C\) the change-of-basis matrix and the composite \(F\) the change-of-frame matrix or passive transformation matrix. Note that this matrix maps coordinates from the new frame to the old frame. The coordinate transformation matrix or active transformation matrix which maps coordinates from the old frame to the new frame is the inverse of the change-of-frame matrix. Note that the inverse may not exist.
3.5.3. Homogeneous coordinates¶
The homogeneous coordinates \((h_1, \ldots, h_n, h_{n+1})\) of point \(a\) with \(h_{i}\in\mathbb{R}\) and \(h_{n+1}\neq 0\) are defined so that
The coordinates \((a_1, \dots, a_n, 1)\) are a special case of homogeneous coordinates.
3.5.4. Homographies¶
A homography is a transformation can be represented be the following change-of-frame matrix
Different transformation types can be distinguished based on the properties of the translation vector \(T\), change-of-basis matrix \(C\) and projection vector \(P\)
translation:
\(C\) is the identity matrix and \(P\) are all zeros
proper rigid (euclidian) transformation:
\(P\) are all zeros
\(C\) is any orthogonal matrix (\(C^T=C^{-1}\)) with determinant \(\det{C}=+1\)
preserves angles, distances and handedness
translation + rotation
rigid (euclidian) transformation:
\(P\) are all zeros
\(C\) is any orthogonal matrix (\(C^T=C^{-1}\))
preserves angles and distances
proper rigid transformation + reflection
similarity transformation:
\(P\) are all zeros
\(C=rA\) where A any orthogonal matrix (\(A^T=A^{-1}\)) and \(r>0\)
preserves angles and ratios between distances
rigid transformation + isotropic scaling
affine transformation:
\(P\) are all zeros
\(C\) is any invertible matrix (i.e. linear transformation)
preserves parallelism
similarity transformation + non-isotropic scaling and shear
projective transformation (homography):
\(C\) is any invertible matrix (i.e. linear transformation)
preserves collinearity
affine transformation + projection
The chained NXtransformations allows the definition of a proper rigid transformations because it is meant to position objects like sample, optics and detectors.
Possitive rotations around the x-, y- and z-axis of the standard Euclidean basis in \(\mathbb{R}^3\) are defined by
3.5.5. Metric tensor¶
If \(A\) and \(B\) are the coordinates of \(\vec{oa}\) and \(\vec{ob}\) with respect to a basis \(C\) given in the standard Euclidean basis (i.e. the columns are the coordinates of the basis in the standard Euclidean basis) then
where \(M\) is the metric tensor of the basis \(C\). Each element of \(M\) is the dot product of two basis vectors
The metrix tensor of the standard Euclidean basis is the identity matrix.
3.5.6. Dual basis¶
The dual basis \((b^1,\dots,b^n)\) of basis \((b_1,\dots,b_n)\) is defined as the basis for which
If \(C_{b}\) and \(C_{b^\ast}\) define a basis and its dual basis with respect to the standard Euclidean basis (i.e. the columns are the coordinates in the standard Euclidean basis) then
and the metrix tensors of the two bases are eachothers inverse
Note: in crystallography the dual basis of a lattice basis is referred to as the recipocal basis which spans reciprocal space. The metrix tensor \(M\) is referred to as the Gram matrix \(G\).
3.5.7. Crystallography¶
3.5.7.1. Lattice¶
In crystallography the lattice parameters \((a, b, c, \alpha, \beta, \gamma)\) describe the metric tensor of the lattice basis
The full description of the lattice basis however is given by \(C\) (the coordinates of the lattic basis vectors with respect to the standard Euclidean basis). The extra three parameters describe the orientation of the lattice basis in the standard Euclidean basis.
The recipical lattice parameters \((a^\ast, b^\ast, c^\ast, \alpha^\ast, \beta^\ast, \gamma^\ast)\) describe the metric tensor in recipocal space
The full description of the reciprocal lattice basis however is given by \(C^\ast\) (the coordinates of the lattic basis vectors with respect to the standard Euclidean basis). The extra three parameters describe the orientation of the recipcrocal lattice basis in the standard Euclidean basis.
3.5.7.2. Diffraction¶
A vector \(\vec{h}\) with integral coordinates \(H^\ast\) with respect to the reciprocal lattice basis is a lattice vector. The coordinates are called Miller indices.
The scattering vector \(\vec{q}\) is the different between the the outgoing and incomming wave vectors \(\vec{k}\) and \(\vec{k}_{0}\)
where \(\lambda\) the wavelength of the primary beam.
Constructive interference can occur if symmetry allows it for a lattice vector \(\vec{h}\) and a primary beam \(\vec{k}_{0}\) in the direction of \(\vec{k}\) when
That’s fancy way of saying we detect a non-zero scattered intensity in the direction of \(\vec{k}\).
3.5.7.3. Busing and Levy convention¶
Busing and H. A. Levy (Acta Cryst. (1967). 22, 457-464) define conventions which are followed by the Nexus standard.
Reciprocal basis¶
The reciprocal basis with respect to the standard Euclidean basis is chosen to be
As a consequence the associated change-of-basis matrix \(C^\ast\), denoted as the matrix \(B\), is given by
Orientation¶
The crystal is randomly mounted on a diffractometer and with all diffractometer angles set to zero, the coordinates \(H\) of a vector \(\vec{h}\) with respect to the standard Euclidean basis can be calculated from the coordinates \(H^\ast\) with respect to the reciprocal lattice basis
where \(U\) the orientation matrix. Integral coordinates \(H^\ast\) are referred to as Miller indices and are the coordinates of the lattice vectors. The inverse of the norm of a lattice vector is the d-spacing and the lattice vector is normal to the lattice planes associated to the Miller indices.
For a 3-circle diffractometer the sample can be rotated by
where \((\omega, \chi, \phi)\) is referred to as the sample orientation.
The NXsample class defines these fields
unit_cell: lattice parameters \((a, b, c, \alpha, \beta, \gamma)\)
orientation_matrix: the orthogonal \(U\) matrix
sample_orientation: 3-circle diffractometer angles \((\omega, \chi, \phi)\)
ub_matrix: the matrix product \(UB\)
3.6. Example utilities¶
Here we define some methods for the examle
3.6.1. Diffraction¶
Some useful helper functions for crystallography
[1]:
import numpy
from scipy import constants
from scipy.constants import physical_constants
def cos(angle_deg):
return numpy.cos(numpy.radians(angle_deg))
def sin(angle_deg):
return numpy.sin(numpy.radians(angle_deg))
def arccos(cosangle):
return numpy.degrees(numpy.arccos(cosangle))
def lattice_to_metrix_tensor(lattice):
aa = lattice["a"] ** 2
bb = lattice["b"] ** 2
cc = lattice["c"] ** 2
bc = lattice["b"] * lattice["c"] * cos(lattice["alpha"])
ac = lattice["a"] * lattice["c"] * cos(lattice["beta"])
ab = lattice["a"] * lattice["b"] * cos(lattice["gamma"])
return numpy.array([[aa, ab, ac], [ab, bb, bc], [ac, bc, cc]])
def metric_tensor_to_lattice(M):
lattice = dict()
lattice["a"] = M[0, 0] ** 0.5
lattice["b"] = M[1, 1] ** 0.5
lattice["c"] = M[2, 2] ** 0.5
lattice["alpha"] = arccos(M[1, 2] / (lattice["b"] * lattice["c"]))
lattice["beta"] = arccos(M[0, 2] / (lattice["a"] * lattice["c"]))
lattice["gamma"] = arccos(M[0, 1] / (lattice["a"] * lattice["b"]))
return lattice
def lattice_to_reciprocal_lattice(lattice):
M = lattice_to_metrix_tensor(lattice)
Mr = numpy.linalg.inv(M)
return metric_tensor_to_lattice(Mr)
def busing_levy_reciprocal_basis(lattice):
rlattice = lattice_to_reciprocal_lattice(lattice)
br1 = [rlattice["a"], 0, 0]
br2 = [
rlattice["b"] * cos(rlattice["gamma"]),
rlattice["b"] * sin(rlattice["gamma"]),
0,
]
br3 = [
rlattice["c"] * cos(rlattice["beta"]),
-rlattice["c"] * sin(rlattice["beta"]) * cos(lattice["alpha"]),
1 / lattice["c"],
]
return numpy.array([br1, br2, br3]).T
def rotx(angle):
c = cos(angle)
s = sin(angle)
return numpy.array([[1, 0, 0], [0, c, -s], [0, s, c]])
def roty(angle):
c = cos(angle)
s = sin(angle)
return numpy.array([[c, 0, -s], [0, 1, 0], [s, 0, c]])
def rotz(angle):
c = cos(angle)
s = sin(angle)
return numpy.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
def simplified_diffraction(Q, C, N):
# Q: p x n (rows are the coordinates)
# C: n x n (columns are the basis vectors)
# N: n (number of unit cells in each direction)
# returns: p
if Q.ndim == 1:
Q = Q[numpy.newaxis, :] # 1 x n
x = (Q.dot(C)).T / 2 # n x p
M = numpy.array(N)[:, numpy.newaxis] # n x 1
with numpy.errstate(divide="ignore", invalid="ignore"):
g = numpy.sin(x * M) ** 2 / numpy.sin(x) ** 2
for gi, Ni in zip(g, N):
gi[numpy.isnan(gi)] = Ni**2
g = numpy.product(g, axis=0)
transmission = numpy.all(numpy.abs(x) < (numpy.pi / 2), axis=0)
g[transmission] *= 2
return g
kev_angstrom_factor = (
constants.h * constants.c * 1e7 / physical_constants["electron volt"][0]
)
def wavelength(energy_keV): # m
return kev_angstrom_factor / energy_keV * 1e10
def wavenumber(energy_keV): # m^-1
return 2 * numpy.pi / wavelength(energy_keV)
def test_utilities():
lattice = {"a": 1, "b": 1.2, "c": 1.3, "alpha": 70, "beta": 60, "gamma": 80}
# Test metric tensor
M = lattice_to_metrix_tensor(lattice)
Mr = numpy.linalg.inv(M)
lattice2 = metric_tensor_to_lattice(M)
numpy.testing.assert_allclose(list(lattice.values()), list(lattice2.values()))
# Test reciprocal lattice basis
B = busing_levy_reciprocal_basis(lattice)
numpy.testing.assert_allclose(Mr, B.T.dot(B))
# Test lattice basis
C = numpy.linalg.inv(B.T)
numpy.testing.assert_allclose(M, C.T.dot(C))
# Test norm
h = numpy.array([1, 2, 3])
norm1 = sum(B.dot(h) ** 2) ** 0.5
norm2 = (h.dot(Mr).dot(h)) ** 0.5
numpy.testing.assert_allclose(norm1, norm2)
# Test diffraction
N = numpy.array([1, 2, 3])
h = numpy.array([2, 2, 0])
Q = 2 * numpy.pi * B.dot(h)
intensity = simplified_diffraction(Q, C, N)
numpy.testing.assert_allclose(intensity, numpy.product(N**2))
h = numpy.array([0, 0, 0])
Q = 2 * numpy.pi * B.dot(h)
intensity = simplified_diffraction(Q, C, N)
numpy.testing.assert_allclose(intensity, 2 * numpy.product(N**2))
# Test d-spacing
a = physical_constants["lattice parameter of silicon"][0] * 1e10
lattice = {"a": a, "b": a, "c": a, "alpha": 90, "beta": 90, "gamma": 90}
M = lattice_to_metrix_tensor(lattice)
Mr = numpy.linalg.inv(M)
h = numpy.array([2, 2, 0])
hnorm = h.dot(Mr.dot(h)) ** 0.5
d220 = physical_constants["lattice spacing of ideal Si (220)"][0] * 1e10
numpy.testing.assert_allclose(d220, 1 / hnorm)
test_utilities()
3.6.2. Plotting¶
Plotting with an affine transformation using matplotlib
[2]:
import matplotlib.pyplot as plt
from matplotlib.transforms import Affine2D
from matplotlib.ticker import MaxNLocator
def _get_extent(signal, axis0, axis1):
assert signal.shape == (axis0.size, axis1.size)
x0, x1 = axis1[[0, -1]]
y0, y1 = axis0[[0, -1]]
dx = (axis1[1] - axis1[0]) / 2
dy = (axis0[1] - axis0[0]) / 2
return x0 - dx, x1 + dx, y0 - dy, y1 + dy
def _get_transformed_extent(signal, axis0, axis1, active):
xmin, xmax, ymin, ymax = _get_extent(signal, axis0, axis1)
corners = [[xmin, ymin, 1], [xmax, ymin, 1], [xmin, ymax, 1], [xmax, ymax, 1]]
corners = numpy.array(corners).T
corners = active.dot(corners)
xmin = corners[0].min()
xmax = corners[0].max()
ymin = corners[1].min()
ymax = corners[1].max()
return xmin, xmax, ymin, ymax
def _imshow(ax, signal, axis0, axis1):
extent = _get_extent(signal, axis0, axis1)
im = ax.imshow(signal, extent=extent, aspect="equal", origin="lower")
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
ax.yaxis.set_major_locator(MaxNLocator(integer=True))
xmin, xmax, ymin, ymax = extent
ax.set_xlim(xmin, xmax)
ax.set_ylim(ymin, ymax)
return im
def plot_nxdata2d(signal, axis0, axis1, **kw):
plt.figure(**kw)
ax = plt.gca()
im = _imshow(ax, signal, axis0, axis1)
plt.colorbar(im)
def flip_xy(matrix):
# [x,y,1] -> [y,x,1]
matrix = matrix.copy()
matrix[0:2, 0:2] = matrix[0:2, 0:2].T
matrix[0:2, 2] = matrix[0:2, 2][::-1]
return matrix
def plot_nxdata2d_transform(signal, axis0, axis1, passive_xy, **kw):
# https://matplotlib.org/stable/gallery/images_contours_and_fields/affine_image.html
plt.figure(**kw)
ax = plt.gca()
im = _imshow(ax, signal, axis0, axis1)
if passive_xy is not None:
passive = numpy.asarray(passive_xy)
active = numpy.linalg.inv(passive)
transform = Affine2D(active)
im.set_transform(transform + ax.transData)
lims = _get_transformed_extent(signal, axis0, axis1, active)
xmin, xmax, ymin, ymax = lims
ax.set_xlim(xmin, xmax)
ax.set_ylim(ymin, ymax)
plt.colorbar(im)
def plot_vector2d(ax, x0, y0, x1, y1, label):
norm = ((x1 - x0) ** 2 + (y1 - y0) ** 2) ** 0.5
ax.arrow(
x0,
y0,
x1 - x0,
y1 - y0,
color="white",
width=norm * 0.01,
length_includes_head=True,
)
ax.text(x1, y1, label, color="white")
def test_plotting():
kw = {"figsize": (5, 5)}
# NXdata
n0, n1 = 2, 3
axis0 = numpy.arange(n0) / (n0 - 1)
axis1 = numpy.arange(n1) / (n1 - 1)
signal = numpy.arange(n0 * n1).reshape(n0, n1)
# NXdata without reference frame
# The coordinates are given with respect to the plot basis
plot_nxdata2d(signal, axis0, axis1, **kw)
plt.gca().set_title("Data frame")
# NXdata with reference frame
a = 10
passive = [[1, 0, -1], [0, 1, -2], [0, 0, 1]]
plot_nxdata2d_transform(signal, axis0, axis1, passive)
plt.gca().set_title("Plotting frame (translated)")
# NXdata with reference frame
a = 10
passive = [[cos(a), -sin(a), 0], [sin(a), cos(a), 0], [0, 0, 1]]
plot_nxdata2d_transform(signal, axis0, axis1, passive)
plt.gca().set_title("Plotting frame (rotated)")
test_plotting()
3.6.3. Saving¶
Utilities for reading and writing NXdata
[3]:
import os
import tempfile
import h5py
def get_filename():
return os.path.join(tempfile.gettempdir(), "nxdata_transform_example.h5")
def save_nxdata(name, signal, axis0, axis1, title, passive_xyz=None):
if passive_xyz is None:
mode = "w"
else:
mode = "a"
with h5py.File(get_filename(), mode=mode) as root:
if passive_xyz is None:
root.attrs["NX_class"] = "NXroot"
root.attrs["default"] = "entry"
entry = root.create_group("entry")
entry.attrs["NX_class"] = "NXentry"
else:
entry = root["entry"]
data = entry.create_group(name)
entry.attrs["default"] = name
data.attrs["NX_class"] = "NXdata"
data.attrs["signal"] = "data"
data.attrs["axes"] = [axis0["name"], axis1["name"]]
data["title"] = title
data["data"] = signal
data[axis0["name"]] = axis0["value"]
data[axis1["name"]] = axis1["value"]
data[axis0["name"]].attrs["long_name"] = axis0["title"]
data[axis1["name"]].attrs["long_name"] = axis1["title"]
if passive_xyz is not None:
data.attrs["passive_transformation"] = "transform"
passive_xy = numpy.identity(3)
passive_xy[:2, :2] = passive_xyz[:2, :2]
passive_xy[:2, -1] = passive_xyz[:2, -1]
data["transform"] = flip_xy(passive_xy)
def plot_nxdata(name, figsize=(10, 10)):
with h5py.File(get_filename(), mode="r") as root:
data = root["entry"][name]
signal = data[data.attrs["signal"]]
axis0 = data[data.attrs["axes"][0]]
axis1 = data[data.attrs["axes"][1]]
transform_name = data.attrs.get("passive_transformation")
if transform_name is None:
plot_nxdata2d(signal, axis0, axis1, figsize=(10, 10))
else:
passive = flip_xy(data[transform_name][()])
plot_nxdata2d_transform(signal, axis0, axis1, passive, figsize=figsize)
ax = plt.gca()
ax.set_title(data["title"][()].decode())
ax.set_xlabel(axis1.attrs["long_name"])
ax.set_ylabel(axis0.attrs["long_name"])
3.7. Examples¶
3.7.1. NXdata crystallography example¶
Assume we measure a diffraction signal in reciprocal space with coordinates in HKL space and we want to plot the data in direct or reciprocal space.
[4]:
# Lattice in direct space
lattice = {"a": 1, "b": 2, "c": 1, "alpha": 90, "beta": 90, "gamma": 120}
Regular sampling grid in HKL space
[5]:
haxis = numpy.linspace(-1.5, 1.5, 400)
kaxis = numpy.linspace(-1.5, 1.5, 400)
h, k = numpy.meshgrid(haxis, kaxis, indexing="xy")
shape = h.shape
h = h.flatten()
k = k.flatten()
l = numpy.zeros_like(h)
HKL = numpy.stack([h, k, l], axis=1)
NXdata axes and signal: diffraction signal with pixel axes
[6]:
# Laue conditions for diffraction
Q = 2 * numpy.pi * HKL
# Number of unit cells in each direction
N = [3, 3, 3]
signal = simplified_diffraction(Q, numpy.identity(3), N).reshape(shape)
hmin = haxis[0]
dh = haxis[1] - haxis[0]
kmin = kaxis[0]
dk = kaxis[1] - kaxis[0]
axis1 = (haxis - hmin) / dh
axis0 = (kaxis - kmin) / dk
Save NXdata with different passive transformations
[7]:
axis0 = {"name": "y", "value": axis0}
axis1 = {"name": "x", "value": axis1}
axis0["title"] = "Y (pixels)"
axis1["title"] = "X (pixels)"
save_nxdata("raw", signal, axis0, axis1, "Raw data frame")
passive_data_to_reciprocal = numpy.identity(4)
passive_data_to_reciprocal[0, -1] = -hmin / dh
passive_data_to_reciprocal[1, -1] = -kmin / dk
passive_data_to_reciprocal[0, 0] = 1 / dh
passive_data_to_reciprocal[1, 1] = 1 / dk
axis0["title"] = "b*"
axis1["title"] = "a*"
save_nxdata(
"reciprocal",
signal,
axis0,
axis1,
"Reciprocal frame",
passive_xyz=passive_data_to_reciprocal,
)
print("Passive transformation: Reciprocal frame")
print(passive_data_to_reciprocal)
B = busing_levy_reciprocal_basis(lattice)
passive_reciprocal_to_standard = numpy.identity(4)
passive_reciprocal_to_standard[:3, :3] = numpy.linalg.inv(B)
passive_data_to_standard = passive_data_to_reciprocal.dot(
passive_reciprocal_to_standard
)
axis0["title"] = "e1"
axis1["title"] = "e0"
save_nxdata(
"standard",
signal,
axis0,
axis1,
"Standard Euclidean frame",
passive_xyz=passive_data_to_standard,
)
print("\nPassive transformation: Standard Euclidean frame")
print(passive_data_to_standard)
Passive transformation: Reciprocal frame
[[133. 0. 0. 199.5]
[ 0. 133. 0. 199.5]
[ 0. 0. 1. 0. ]
[ 0. 0. 0. 1. ]]
Passive transformation: Standard Euclidean frame
[[ 1.15181379e+02 -6.65000000e+01 -1.11247759e-14 1.99500000e+02]
[ 0.00000000e+00 2.66000000e+02 1.62878024e-14 1.99500000e+02]
[ 0.00000000e+00 0.00000000e+00 1.00000000e+00 0.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00]]
The final plot we want to show
[8]:
passive_xyz = passive_data_to_standard
passive_xy = numpy.identity(3)
passive_xy[:2, :2] = passive_xyz[:2, :2]
passive_xy[:2, -1] = passive_xyz[:2, -1]
plot_nxdata2d_transform(
signal, axis0["value"], axis1["value"], passive_xy, figsize=(10, 4)
)
ax = plt.gca()
ax.set_title("Standard Euclidean frame")
ax.set_xlabel("e0")
ax.set_ylabel("e1")
rlattice = lattice_to_reciprocal_lattice(lattice)
ra = rlattice["a"]
rb = rlattice["b"]
rab = rlattice["gamma"]
plot_vector2d(ax, 0, 0, ra, 0, "(1, 0, 0)")
plot_vector2d(ax, 0, 0, rb * cos(rab), rb * sin(rab), "(0, 1, 0)")
Plot data without transformation
[9]:
plot_nxdata("raw")
ax = plt.gca()
x0, y0, *_ = passive_data_to_reciprocal.dot([0, 0, 0, 1])[:2]
x1, y1, *_ = passive_data_to_reciprocal.dot([1, 0, 0, 1])[:2]
plot_vector2d(ax, x0, y0, x1, y1, "(1, 0, 0)")
x1, y1, *_ = passive_data_to_reciprocal.dot([0, 1, 0, 1])[:2]
plot_vector2d(ax, x0, y0, x1, y1, "(0, 1, 0)")
Plot data in reciprocal space
[10]:
plot_nxdata("reciprocal")
ax = plt.gca()
plot_vector2d(ax, 0, 0, 1, 0, "(1, 0, 0)")
plot_vector2d(ax, 0, 0, 0, 1, "(0, 1, 0)")
Plot data in the standard Euclidean frame
[11]:
plot_nxdata("standard", figsize=(10, 4))
reciprocal_to_standard = numpy.identity(4)
reciprocal_to_standard[:3, :3] = B
ax = plt.gca()
x0, y0, *_ = reciprocal_to_standard.dot([0, 0, 0, 1])[:2]
x1, y1, *_ = reciprocal_to_standard.dot([1, 0, 0, 1])[:2]
plot_vector2d(ax, x0, y0, x1, y1, "(1, 0, 0)")
x1, y1, *_ = reciprocal_to_standard.dot([0, 1, 0, 1])[:2]
plot_vector2d(ax, x0, y0, x1, y1, "(0, 1, 0)")