# 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) 2018-2020, 2023 Dominik Kriegner <dominik.kriegner@gmail.com>
import math
import numpy
from .. import utilities
[docs]
def show_reciprocal_space_plane(
mat, exp, ttmax=None, maxqout=0.01, scalef=100, ax=None, color=None,
show_Laue=True, show_legend=True, projection='perpendicular',
label=None, **kwargs):
"""
show a plot of the coplanar diffraction plane with peak positions for the
respective material. the size of the spots is scaled with the strength of
the structure factor
Parameters
----------
mat: Crystal
instance of Crystal for structure factor calculations
exp: Experiment
instance of Experiment (needs to be HXRD, or FourC for onclick action
to work correctly). defines the inplane and out of plane direction as
well as the sample azimuth
ttmax: float, optional
maximal 2Theta angle to consider, by default 180deg
maxqout: float, optional
maximal out of plane q for plotted Bragg peaks as fraction of exp.k0
scalef: float, or callable, optional
scale factor or function for the marker size. If this is a function it
should take only one float argument and return another float which is
used as 's' parameter in matplotlib.pyplot.scatter
ax: matplotlib.Axes, optional
matplotlib Axes to use for the plot, useful if multiple materials
should be plotted in one plot
color: matplotlib color, optional
show_Laue: bool, optional
flag to indicate if the Laue zones should be indicated
show_legend: bool, optional
flag to indiate if a legend should be shown
projection: 'perpendicular', 'polar', optional
type of projection for Bragg peaks which do not fall into the
diffraction plane. 'perpendicular' (default) uses only the inplane
component in the scattering plane, whereas 'polar' uses the vectorial
absolute value of the two inplane components. See also the 'maxqout'
option.
label: None or str, optional
label to be used for the legend. If 'None' the name of the material
will be used.
kwargs: optional
kwargs are forwarded to matplotlib.pyplot.scatter and allow to change
the appearance of the points.
Returns
-------
Axes, plot_handle
"""
def get_peaks(mat, exp, ttmax):
"""
Parameters
----------
mat: Crystal
instance of Crystal for structure factor calculations
exp: Experiment
instance of Experiment (likely HXRD, or FourC)
tt_cutoff: float
maximal 2Theta angle to consider, by default 180deg
Returns
-------
ndarray
data array with columns for 'q', 'qvec', 'hkl', 'r' for the Bragg
peaks
"""
qmax = 2 * exp.k0 * math.sin(math.radians(ttmax/2.))
hkls = tuple(mat.lattice.get_allowed_hkl(qmax))
q = mat.Q(hkls)
data = numpy.zeros(len(hkls), dtype=[('qx', numpy.double),
('qy', numpy.double),
('qz', numpy.double),
('r', numpy.double),
('hkl', numpy.ndarray)])
qvec = exp.Transform(q)
data['qx'] = qvec[:, 0]
data['qy'] = qvec[:, 1]
data['qz'] = qvec[:, 2]
rref = abs(mat.StructureFactor((0, 0, 0), exp.energy)) ** 2
data['r'] = numpy.abs(mat.StructureFactorForQ(q, exp.energy)) ** 2
data['r'] /= rref
data['hkl'] = hkls
return data
plot, plt = utilities.import_matplotlib_pyplot('XU.materials')
if not plot:
print('matplotlib needed for show_reciprocal_space_plane')
return None, None # return values for consistency with signature below
if ttmax is None:
ttmax = 180
d = get_peaks(mat, exp, ttmax)
k0 = exp.k0
if not ax:
fig = plt.figure(figsize=(9, 5))
ax = plt.subplot(111)
else:
fig = ax.get_figure()
plt.sca(ax)
plt.axis('scaled')
ax.set_autoscaley_on(False)
ax.set_autoscalex_on(False)
plt.xlim(-2.05*k0, 2.05*k0)
plt.ylim(-0.05*k0, 2.05*k0)
if show_Laue:
c = plt.matplotlib.patches.Circle((0, 0), 2*k0, facecolor='#FF9180',
edgecolor='none')
ax.add_patch(c)
qmax = 2 * k0 * math.sin(math.radians(ttmax/2.))
c = plt.matplotlib.patches.Circle((0, 0), qmax, facecolor='#FFFFFF',
edgecolor='none')
ax.add_patch(c)
c = plt.matplotlib.patches.Circle((0, 0), 2*k0, facecolor='none',
edgecolor='0.5')
ax.add_patch(c)
c = plt.matplotlib.patches.Circle((k0, 0), k0, facecolor='none',
edgecolor='0.5')
ax.add_patch(c)
c = plt.matplotlib.patches.Circle((-k0, 0), k0, facecolor='none',
edgecolor='0.5')
ax.add_patch(c)
plt.hlines(0, -2*k0, 2*k0, color='0.5', lw=0.5)
plt.vlines(0, -2*k0, 2*k0, color='0.5', lw=0.5)
# mask for plotting
m = numpy.abs(d['qx']) < maxqout*k0
if projection == 'perpendicular':
x = d['qy'][m]
else:
x = numpy.sign(d['qy'][m])*numpy.sqrt(d['qx'][m]**2 + d['qy'][m]**2)
y = d['qz'][m]
s = numpy.empty_like(d['r'][m])
if callable(scalef):
s[...] = [scalef(r) for r in d['r'][m]]
else:
s = d['r'][m]*scalef
kwargs.setdefault("label", label if label else mat.name)
kwargs.setdefault("zorder", 2)
kwargs.setdefault("s", s)
kwargs.setdefault("c", color)
h = plt.scatter(x, y, **kwargs)
plt.xlabel(r'$Q$ inplane ($\mathrm{\AA^{-1}}$)')
plt.ylabel(r'$Q$ out of plane ($\mathrm{\AA^{-1}}$)')
if show_legend:
if len(fig.legends) == 1:
fig.legends[0].remove()
fig.legend(*ax.get_legend_handles_labels(), loc='upper right')
plt.tight_layout()
annot = ax.annotate("", xy=(0, 0), xytext=(20, 20),
textcoords="offset points",
bbox=dict(boxstyle="round", fc="w"),
arrowprops=dict(arrowstyle="->"))
annot.set_visible(False)
def update_annot(ind):
pos = h.get_offsets()[ind["ind"][0]]
annot.xy = pos
text = f"{mat.name}\n{str(d['hkl'][m][ind['ind'][0]])}"
annot.set_text(text)
if h.get_facecolor().size > 0:
color = h.get_facecolor()[0]
elif h.get_edgecolor().size > 0:
color = h.get_edgecolor()[0]
else:
color = 'w'
annot.get_bbox_patch().set_facecolor(color)
annot.get_bbox_patch().set_alpha(0.2)
def hover(event):
vis = annot.get_visible()
if event.inaxes == ax:
cont, ind = h.contains(event)
if cont:
update_annot(ind)
annot.set_visible(True)
fig.canvas.draw_idle()
else:
if vis:
annot.set_visible(False)
fig.canvas.draw_idle()
def click(event):
if event.inaxes == ax:
cont, ind = h.contains(event)
if cont:
popts = numpy.get_printoptions()
numpy.set_printoptions(precision=4, suppress=True)
q = (d['qx'][m][ind['ind'][0]], d['qy'][m][ind['ind'][0]],
d['qz'][m][ind['ind'][0]])
angles = exp.Q2Ang(q, trans=False, geometry='real')
text = f"""{mat.name}
hkl: {d['hkl'][m][ind['ind'][0]]}
exp.Q2Ang angles (om, tilt, azimuth, 2th): {angles}"""
numpy.set_printoptions(**popts)
print(text)
fig.canvas.mpl_connect("motion_notify_event", hover)
fig.canvas.mpl_connect("button_press_event", click)
return ax, h