ChessAnalysisPipeline 0.0.17.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- CHAP/TaskManager.py +216 -0
- CHAP/__init__.py +27 -0
- CHAP/common/__init__.py +57 -0
- CHAP/common/models/__init__.py +8 -0
- CHAP/common/models/common.py +124 -0
- CHAP/common/models/integration.py +659 -0
- CHAP/common/models/map.py +1291 -0
- CHAP/common/processor.py +2869 -0
- CHAP/common/reader.py +658 -0
- CHAP/common/utils.py +110 -0
- CHAP/common/writer.py +730 -0
- CHAP/edd/__init__.py +23 -0
- CHAP/edd/models.py +876 -0
- CHAP/edd/processor.py +3069 -0
- CHAP/edd/reader.py +1023 -0
- CHAP/edd/select_material_params_gui.py +348 -0
- CHAP/edd/utils.py +1572 -0
- CHAP/edd/writer.py +26 -0
- CHAP/foxden/__init__.py +19 -0
- CHAP/foxden/models.py +71 -0
- CHAP/foxden/processor.py +124 -0
- CHAP/foxden/reader.py +224 -0
- CHAP/foxden/utils.py +80 -0
- CHAP/foxden/writer.py +168 -0
- CHAP/giwaxs/__init__.py +11 -0
- CHAP/giwaxs/models.py +491 -0
- CHAP/giwaxs/processor.py +776 -0
- CHAP/giwaxs/reader.py +8 -0
- CHAP/giwaxs/writer.py +8 -0
- CHAP/inference/__init__.py +7 -0
- CHAP/inference/processor.py +69 -0
- CHAP/inference/reader.py +8 -0
- CHAP/inference/writer.py +8 -0
- CHAP/models.py +227 -0
- CHAP/pipeline.py +479 -0
- CHAP/processor.py +125 -0
- CHAP/reader.py +124 -0
- CHAP/runner.py +277 -0
- CHAP/saxswaxs/__init__.py +7 -0
- CHAP/saxswaxs/processor.py +8 -0
- CHAP/saxswaxs/reader.py +8 -0
- CHAP/saxswaxs/writer.py +8 -0
- CHAP/server.py +125 -0
- CHAP/sin2psi/__init__.py +7 -0
- CHAP/sin2psi/processor.py +8 -0
- CHAP/sin2psi/reader.py +8 -0
- CHAP/sin2psi/writer.py +8 -0
- CHAP/tomo/__init__.py +15 -0
- CHAP/tomo/models.py +210 -0
- CHAP/tomo/processor.py +3862 -0
- CHAP/tomo/reader.py +9 -0
- CHAP/tomo/writer.py +59 -0
- CHAP/utils/__init__.py +6 -0
- CHAP/utils/converters.py +188 -0
- CHAP/utils/fit.py +2947 -0
- CHAP/utils/general.py +2655 -0
- CHAP/utils/material.py +274 -0
- CHAP/utils/models.py +595 -0
- CHAP/utils/parfile.py +224 -0
- CHAP/writer.py +122 -0
- MLaaS/__init__.py +0 -0
- MLaaS/ktrain.py +205 -0
- MLaaS/mnist_img.py +83 -0
- MLaaS/tfaas_client.py +371 -0
- chessanalysispipeline-0.0.17.dev3.dist-info/LICENSE +60 -0
- chessanalysispipeline-0.0.17.dev3.dist-info/METADATA +29 -0
- chessanalysispipeline-0.0.17.dev3.dist-info/RECORD +70 -0
- chessanalysispipeline-0.0.17.dev3.dist-info/WHEEL +5 -0
- chessanalysispipeline-0.0.17.dev3.dist-info/entry_points.txt +2 -0
- chessanalysispipeline-0.0.17.dev3.dist-info/top_level.txt +2 -0
CHAP/edd/utils.py
ADDED
|
@@ -0,0 +1,1572 @@
|
|
|
1
|
+
"""Utility functions for EDD workflows."""
|
|
2
|
+
|
|
3
|
+
# System modules
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
|
|
6
|
+
# Third party modules
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
# Local modules
|
|
10
|
+
from CHAP.utils.general import fig_to_iobuf
|
|
11
|
+
|
|
12
|
+
def get_peak_locations(ds, tth):
|
|
13
|
+
"""Return the peak locations for a given set of lattice spacings
|
|
14
|
+
and 2&theta value.
|
|
15
|
+
|
|
16
|
+
:param ds: A set of lattice spacings in angstroms.
|
|
17
|
+
:type ds: list[float]
|
|
18
|
+
:param tth: Diffraction angle 2&theta in degrees.
|
|
19
|
+
:type tth: float
|
|
20
|
+
:return: The peak locations in keV.
|
|
21
|
+
:rtype: numpy.ndarray
|
|
22
|
+
"""
|
|
23
|
+
# Third party modules
|
|
24
|
+
from scipy.constants import physical_constants
|
|
25
|
+
|
|
26
|
+
hc = 1e7 * physical_constants['Planck constant in eV/Hz'][0] \
|
|
27
|
+
* physical_constants['speed of light in vacuum'][0]
|
|
28
|
+
|
|
29
|
+
return hc / (2. * ds * np.sin(0.5 * np.radians(tth)))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def make_material(name, sgnum, lattice_parameters, dmin=0.6):
|
|
33
|
+
"""Return a hexrd.material.Material with the given properties.
|
|
34
|
+
|
|
35
|
+
:param name: Material name.
|
|
36
|
+
:type name: str
|
|
37
|
+
:param sgnum: Space group of the material.
|
|
38
|
+
:type sgnum: int
|
|
39
|
+
:param lattice_parameters: The material's lattice parameters
|
|
40
|
+
([a, b, c, α, β, γ], or fewer as the symmetry of
|
|
41
|
+
the space group allows --- for instance, a cubic lattice with
|
|
42
|
+
space group number 225 can just provide [a, ]).
|
|
43
|
+
:type lattice_parameters: list[float]
|
|
44
|
+
:param dmin: Materials's dmin value in angstroms (Å),
|
|
45
|
+
defaults to `0.6`.
|
|
46
|
+
:type dmin: float, optional
|
|
47
|
+
:return: A hexrd material.
|
|
48
|
+
:rtype: heard.material.Material
|
|
49
|
+
"""
|
|
50
|
+
# Third party modules
|
|
51
|
+
from hexrd.material import Material
|
|
52
|
+
from hexrd.valunits import valWUnit
|
|
53
|
+
|
|
54
|
+
material = Material()
|
|
55
|
+
material.name = name
|
|
56
|
+
material.sgnum = sgnum
|
|
57
|
+
if isinstance(lattice_parameters, float):
|
|
58
|
+
lattice_parameters = [lattice_parameters]
|
|
59
|
+
material.latticeParameters = lattice_parameters
|
|
60
|
+
material.dmin = valWUnit('lp', 'length', dmin, 'angstrom')
|
|
61
|
+
nhkls = len(material.planeData.exclusions)
|
|
62
|
+
material.planeData.set_exclusions(np.zeros(nhkls, dtype=bool))
|
|
63
|
+
|
|
64
|
+
return material
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_unique_hkls_ds(materials, tth_max=None, tth_tol=None, round_sig=8):
|
|
68
|
+
"""Return the unique HKLs and lattice spacings for the given list
|
|
69
|
+
of materials.
|
|
70
|
+
|
|
71
|
+
:param materials: Materials to get HKLs and lattice spacings for.
|
|
72
|
+
:type materials: list[hexrd.material.Material]
|
|
73
|
+
:param tth_max: Detector rotation about hutch x axis.
|
|
74
|
+
:type tth_max: float, optional
|
|
75
|
+
:param tth_tol: Minimum resolvable difference in 2&theta between
|
|
76
|
+
two unique HKL peaks.
|
|
77
|
+
:type tth_tol: float, optional
|
|
78
|
+
:param round_sig: The number of significant figures in the unique
|
|
79
|
+
lattice spacings, defaults to `8`.
|
|
80
|
+
:type round_sig: int, optional
|
|
81
|
+
:return: Unique HKLs, unique lattice spacings.
|
|
82
|
+
:rtype: tuple[np.ndarray, np.ndarray]
|
|
83
|
+
"""
|
|
84
|
+
# Local modules
|
|
85
|
+
from CHAP.edd.models import MaterialConfig
|
|
86
|
+
|
|
87
|
+
_materials = deepcopy(materials)
|
|
88
|
+
for i, m in enumerate(materials):
|
|
89
|
+
if isinstance(m, MaterialConfig):
|
|
90
|
+
_materials[i] = m._material
|
|
91
|
+
hkls = np.empty((0,3))
|
|
92
|
+
ds = np.empty((0))
|
|
93
|
+
ds_index = np.empty((0))
|
|
94
|
+
for i, material in enumerate(_materials):
|
|
95
|
+
plane_data = material.planeData
|
|
96
|
+
if tth_max is not None:
|
|
97
|
+
plane_data.exclusions = None
|
|
98
|
+
plane_data.tThMax = np.radians(tth_max)
|
|
99
|
+
if tth_tol is not None:
|
|
100
|
+
plane_data.tThWidth = np.radians(tth_tol)
|
|
101
|
+
hkls = np.vstack((hkls, plane_data.hkls.T))
|
|
102
|
+
ds_i = plane_data.getPlaneSpacings()
|
|
103
|
+
ds = np.hstack((ds, ds_i))
|
|
104
|
+
ds_index = np.hstack((ds_index, i*np.ones(len(ds_i))))
|
|
105
|
+
# Sort lattice spacings in reverse order (use -)
|
|
106
|
+
ds_unique, ds_index_unique, _ = np.unique(
|
|
107
|
+
-ds.round(round_sig), return_index=True, return_counts=True)
|
|
108
|
+
ds_unique = np.abs(ds_unique)
|
|
109
|
+
# Limit the list to unique lattice spacings
|
|
110
|
+
hkls_unique = hkls[ds_index_unique,:].astype(int)
|
|
111
|
+
ds_unique = ds[ds_index_unique]
|
|
112
|
+
|
|
113
|
+
return hkls_unique, ds_unique
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def select_tth_initial_guess(x, y, hkls, ds, tth_initial_guess=5.0,
|
|
117
|
+
detector_id=None, interactive=False, return_buf=False):
|
|
118
|
+
"""Show a matplotlib figure of a reference MCA spectrum on top of
|
|
119
|
+
HKL locations. The figure includes an input field to adjust the
|
|
120
|
+
initial 2&theta guess and responds by updating the HKL locations
|
|
121
|
+
based on the adjusted value of the initial 2&theta guess.
|
|
122
|
+
|
|
123
|
+
:param x: MCA channel energies.
|
|
124
|
+
:type x: np.ndarray
|
|
125
|
+
:param y: MCA intensities.
|
|
126
|
+
:type y: np.ndarray
|
|
127
|
+
:param hkls: List of unique HKL indices to fit peaks for in the
|
|
128
|
+
calibration routine.
|
|
129
|
+
:type hkls: Union(numpy.ndarray, list[list[int, int,int]])
|
|
130
|
+
:param ds: Lattice spacings in angstroms associated with the
|
|
131
|
+
unique HKL indices.
|
|
132
|
+
:type ds: Union(numpy.ndarray, list[float])
|
|
133
|
+
:param tth_initial_guess: Initial guess for 2&theta,
|
|
134
|
+
defaults to `5.0`.
|
|
135
|
+
:type tth_initial_guess: float, optional
|
|
136
|
+
:param interactive: Show the plot and allow user interactions with
|
|
137
|
+
the matplotlib figure, defaults to `True`.
|
|
138
|
+
:type interactive: bool, optional
|
|
139
|
+
:param detector_id: Detector ID.
|
|
140
|
+
:type detector_id: str, optional
|
|
141
|
+
:param return_buf: Return an in-memory object as a byte stream
|
|
142
|
+
represention of the Matplotlib figure, defaults to `False`.
|
|
143
|
+
:type return_buf: bool, optional
|
|
144
|
+
:return: The selected initial guess for 2&theta and a byte stream
|
|
145
|
+
represention of the Matplotlib figure if return_buf is `True`
|
|
146
|
+
(`None` otherwise).
|
|
147
|
+
:rtype: float, Union[io.BytesIO, None]
|
|
148
|
+
"""
|
|
149
|
+
if not interactive and not return_buf:
|
|
150
|
+
return tth_initial_guess
|
|
151
|
+
|
|
152
|
+
# Third party modules
|
|
153
|
+
import matplotlib.pyplot as plt
|
|
154
|
+
from matplotlib.widgets import (
|
|
155
|
+
Button,
|
|
156
|
+
TextBox,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def change_fig_title(title):
|
|
160
|
+
"""Change the figure title."""
|
|
161
|
+
if detector_id is not None:
|
|
162
|
+
title = f'Detector {detector_id}: {title}'
|
|
163
|
+
if fig_title:
|
|
164
|
+
fig_title[0].remove()
|
|
165
|
+
fig_title.pop()
|
|
166
|
+
fig_title.append(plt.figtext(*title_pos, title, **title_props))
|
|
167
|
+
|
|
168
|
+
def change_error_text(error):
|
|
169
|
+
"""Change the error text."""
|
|
170
|
+
if error_texts:
|
|
171
|
+
error_texts[0].remove()
|
|
172
|
+
error_texts.pop()
|
|
173
|
+
error_texts.append(plt.figtext(*error_pos, error, **error_props))
|
|
174
|
+
|
|
175
|
+
def new_guess(tth):
|
|
176
|
+
"""Callback function for the tth input."""
|
|
177
|
+
try:
|
|
178
|
+
tth_new_guess = float(tth)
|
|
179
|
+
except Exception:
|
|
180
|
+
change_error_text(
|
|
181
|
+
r'Invalid 2$\theta$ 'f'cannot convert {tth} to float, '
|
|
182
|
+
r'enter a valid 2$\theta$')
|
|
183
|
+
return
|
|
184
|
+
for i, (loc, hkl) in enumerate(zip(
|
|
185
|
+
get_peak_locations(ds, tth_new_guess), hkls)):
|
|
186
|
+
if i in hkl_peaks:
|
|
187
|
+
j = hkl_peaks.index(i)
|
|
188
|
+
hkl_lines[j].remove()
|
|
189
|
+
hkl_lbls[j].remove()
|
|
190
|
+
if x[0] <= loc <= x[-1]:
|
|
191
|
+
hkl_lines[j] = ax.axvline(loc, c='k', ls='--', lw=1)
|
|
192
|
+
hkl_lbls[j] = ax.text(loc, 1, str(hkls[i])[1:-1],
|
|
193
|
+
ha='right', va='top', rotation=90,
|
|
194
|
+
transform=ax.get_xaxis_transform())
|
|
195
|
+
else:
|
|
196
|
+
hkl_peaks.pop(j)
|
|
197
|
+
hkl_lines.pop(j)
|
|
198
|
+
hkl_lbls.pop(j)
|
|
199
|
+
elif x[0] <= loc <= x[-1]:
|
|
200
|
+
hkl_peaks.append(i)
|
|
201
|
+
hkl_lines.append(ax.axvline(loc, c='k', ls='--', lw=1))
|
|
202
|
+
hkl_lbls.append(
|
|
203
|
+
ax.text(
|
|
204
|
+
loc, 1, str(hkl)[1:-1], ha='right', va='top',
|
|
205
|
+
rotation=90, transform=ax.get_xaxis_transform()))
|
|
206
|
+
ax.get_figure().canvas.draw()
|
|
207
|
+
|
|
208
|
+
def confirm(event):
|
|
209
|
+
"""Callback function for the "Confirm" button."""
|
|
210
|
+
plt.close()
|
|
211
|
+
|
|
212
|
+
fig_title = []
|
|
213
|
+
error_texts = []
|
|
214
|
+
|
|
215
|
+
assert np.asarray(hkls).shape[1] == 3
|
|
216
|
+
assert np.asarray(ds).size == np.asarray(hkls).shape[0]
|
|
217
|
+
|
|
218
|
+
# Setup the Matplotlib figure
|
|
219
|
+
title_pos = (0.5, 0.95)
|
|
220
|
+
title_props = {'fontsize': 'xx-large', 'ha': 'center', 'va': 'bottom'}
|
|
221
|
+
error_pos = (0.5, 0.90)
|
|
222
|
+
error_props = {'fontsize': 'x-large', 'ha': 'center', 'va': 'bottom'}
|
|
223
|
+
|
|
224
|
+
fig, ax = plt.subplots(figsize=(11, 8.5))
|
|
225
|
+
ax.plot(x, y)
|
|
226
|
+
ax.set_xlabel('Energy (keV)')
|
|
227
|
+
ax.set_ylabel('Intensity (counts)')
|
|
228
|
+
ax.set_xlim(x[0], x[-1])
|
|
229
|
+
peak_locations = get_peak_locations(ds, tth_initial_guess)
|
|
230
|
+
hkl_peaks = [i for i, loc in enumerate(peak_locations)
|
|
231
|
+
if x[0] <= loc <= x[-1]]
|
|
232
|
+
hkl_lines = [ax.axvline(loc, c='k', ls='--', lw=1) \
|
|
233
|
+
for loc in peak_locations[hkl_peaks]]
|
|
234
|
+
hkl_lbls = [ax.text(loc, 1, str(hkl)[1:-1],
|
|
235
|
+
ha='right', va='top', rotation=90,
|
|
236
|
+
transform=ax.get_xaxis_transform())
|
|
237
|
+
for loc, hkl in zip(peak_locations[hkl_peaks], hkls)]
|
|
238
|
+
|
|
239
|
+
if not interactive:
|
|
240
|
+
|
|
241
|
+
change_fig_title(r'Initial guess for 2$\theta$='f'{tth_initial_guess}')
|
|
242
|
+
|
|
243
|
+
else:
|
|
244
|
+
|
|
245
|
+
change_fig_title(r'Adjust initial guess for 2$\theta$')
|
|
246
|
+
fig.subplots_adjust(bottom=0.2)
|
|
247
|
+
|
|
248
|
+
# Setup tth input
|
|
249
|
+
tth_input = TextBox(plt.axes([0.125, 0.05, 0.15, 0.075]),
|
|
250
|
+
'$2\\theta$: ',
|
|
251
|
+
initial=tth_initial_guess)
|
|
252
|
+
cid_update_tth = tth_input.on_submit(new_guess)
|
|
253
|
+
|
|
254
|
+
# Setup "Confirm" button
|
|
255
|
+
confirm_btn = Button(plt.axes([0.75, 0.05, 0.15, 0.075]), 'Confirm')
|
|
256
|
+
confirm_cid = confirm_btn.on_clicked(confirm)
|
|
257
|
+
|
|
258
|
+
# Show figure for user interaction
|
|
259
|
+
plt.show()
|
|
260
|
+
|
|
261
|
+
# Disconnect all widget callbacks when figure is closed
|
|
262
|
+
tth_input.disconnect(cid_update_tth)
|
|
263
|
+
confirm_btn.disconnect(confirm_cid)
|
|
264
|
+
|
|
265
|
+
# ...and remove the buttons before returning the figure
|
|
266
|
+
tth_input.ax.remove()
|
|
267
|
+
confirm_btn.ax.remove()
|
|
268
|
+
|
|
269
|
+
# Save the figures if requested and close
|
|
270
|
+
if return_buf:
|
|
271
|
+
if interactive:
|
|
272
|
+
title = r'Initial guess for 2$\theta$='f'{tth_input.text}'
|
|
273
|
+
if detector_id is not None:
|
|
274
|
+
title = f'Detector {detector_id}: {title}'
|
|
275
|
+
fig_title[0]._text = title
|
|
276
|
+
fig_title[0].set_in_layout(True)
|
|
277
|
+
fig.tight_layout(rect=(0, 0, 1, 0.95))
|
|
278
|
+
buf = fig_to_iobuf(fig)
|
|
279
|
+
else:
|
|
280
|
+
buf = None
|
|
281
|
+
plt.close()
|
|
282
|
+
|
|
283
|
+
if not interactive:
|
|
284
|
+
tth_new_guess = tth_initial_guess
|
|
285
|
+
else:
|
|
286
|
+
try:
|
|
287
|
+
tth_new_guess = float(tth_input.text)
|
|
288
|
+
except Exception:
|
|
289
|
+
tth_new_guess = select_tth_initial_guess(
|
|
290
|
+
x, y, hkls, ds, tth_initial_guess, interactive, return_buf)
|
|
291
|
+
|
|
292
|
+
return tth_new_guess, buf
|
|
293
|
+
|
|
294
|
+
def select_material_params(
|
|
295
|
+
x, y, tth, preselected_materials=None, label='Reference Data',
|
|
296
|
+
interactive=False, return_buf=False):
|
|
297
|
+
"""Interactively select the lattice parameters and space group for
|
|
298
|
+
a list of materials. A matplotlib figure will be shown with a plot
|
|
299
|
+
of the reference data (`x` and `y`). The figure will contain
|
|
300
|
+
widgets to modify, add, or remove materials. The HKLs for the
|
|
301
|
+
materials defined by the widgets' values will be shown over the
|
|
302
|
+
reference data and updated when the widgets' values are
|
|
303
|
+
updated.
|
|
304
|
+
|
|
305
|
+
:param x: MCA channel energies.
|
|
306
|
+
:type x: np.ndarray
|
|
307
|
+
:param y: MCA intensities.
|
|
308
|
+
:type y: np.ndarray
|
|
309
|
+
:param tth: The (calibrated) 2&theta angle.
|
|
310
|
+
:type tth: float
|
|
311
|
+
:param preselected_materials: Materials to get HKLs and
|
|
312
|
+
lattice spacings for.
|
|
313
|
+
:type preselected_materials: list[hexrd.material.Material],
|
|
314
|
+
optional
|
|
315
|
+
:param label: Legend label for the 1D plot of reference MCA data
|
|
316
|
+
from the parameters `x`, `y`, defaults to `"Reference Data"`.
|
|
317
|
+
:type label: str, optional
|
|
318
|
+
:param interactive: Show the plot and allow user interactions with
|
|
319
|
+
the matplotlib figure, defaults to `False`.
|
|
320
|
+
:type interactive: bool, optional
|
|
321
|
+
:param return_buf: Return an in-memory object as a byte stream
|
|
322
|
+
represention of the Matplotlib figure, defaults to `False`.
|
|
323
|
+
:type return_buf: bool, optional
|
|
324
|
+
:return: The selected materials for the strain analyses and a byte
|
|
325
|
+
stream represention of the Matplotlib figure if return_buf is
|
|
326
|
+
`True` (`None` otherwise).
|
|
327
|
+
:rtype: list[CHAP.edd.models.MaterialConfig],
|
|
328
|
+
Union[io.BytesIO, None]
|
|
329
|
+
"""
|
|
330
|
+
if not interactive and not return_buf:
|
|
331
|
+
if preselected_materials is None:
|
|
332
|
+
raise RuntimeError(
|
|
333
|
+
'If the material properties are not explicitly provided, '
|
|
334
|
+
'the pipeline must be run with `interactive=True`.')
|
|
335
|
+
return preselected_materials, None
|
|
336
|
+
|
|
337
|
+
# Third party modules
|
|
338
|
+
from hexrd.material import Material
|
|
339
|
+
# from CHAP.utils.material import Material
|
|
340
|
+
import matplotlib.pyplot as plt
|
|
341
|
+
from matplotlib.widgets import (
|
|
342
|
+
Button,
|
|
343
|
+
RadioButtons,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Local modules
|
|
347
|
+
from CHAP.edd.models import MaterialConfig
|
|
348
|
+
from CHAP.utils.general import round_to_n
|
|
349
|
+
|
|
350
|
+
def add_material(new_material):
|
|
351
|
+
"""Add a new material to the selected materials."""
|
|
352
|
+
if isinstance(new_material, Material):
|
|
353
|
+
m = new_material
|
|
354
|
+
else:
|
|
355
|
+
if not isinstance(new_material, MaterialConfig):
|
|
356
|
+
new_material = MaterialConfig(**new_material)
|
|
357
|
+
m = new_material._material
|
|
358
|
+
materials.append(m)
|
|
359
|
+
lat_params = [round_to_n(m.latticeParameters[i].value, 6)
|
|
360
|
+
for i in range(6)]
|
|
361
|
+
bottom = 0.05*len(materials)
|
|
362
|
+
if interactive:
|
|
363
|
+
bottom += 0.075
|
|
364
|
+
mat_texts.append(
|
|
365
|
+
plt.figtext(
|
|
366
|
+
0.15, bottom,
|
|
367
|
+
f'- {m.name}: sgnum = {m.sgnum}, lat params = {lat_params}',
|
|
368
|
+
fontsize='large', ha='left', va='center'))
|
|
369
|
+
|
|
370
|
+
def modify(event):
|
|
371
|
+
"""Callback function for the "Modify" button."""
|
|
372
|
+
# Select material
|
|
373
|
+
for mat_text in mat_texts:
|
|
374
|
+
mat_text.remove()
|
|
375
|
+
mat_texts.clear()
|
|
376
|
+
for button in buttons:
|
|
377
|
+
button[0].disconnect(button[1])
|
|
378
|
+
button[0].ax.remove()
|
|
379
|
+
buttons.clear()
|
|
380
|
+
modified_material.clear()
|
|
381
|
+
if len(materials) == 1:
|
|
382
|
+
modified_material.append(materials[0].name)
|
|
383
|
+
plt.close()
|
|
384
|
+
else:
|
|
385
|
+
def modify_material(label):
|
|
386
|
+
modified_material.append(label)
|
|
387
|
+
radio_btn.disconnect(radio_cid)
|
|
388
|
+
radio_btn.ax.remove()
|
|
389
|
+
# Needed to work around a bug in Matplotlib:
|
|
390
|
+
radio_btn.active = False
|
|
391
|
+
plt.close()
|
|
392
|
+
|
|
393
|
+
mat_texts.append(
|
|
394
|
+
plt.figtext(
|
|
395
|
+
0.1, 0.1 + 0.05*len(materials),
|
|
396
|
+
'Select a material to modify:',
|
|
397
|
+
fontsize='x-large', ha='left', va='center'))
|
|
398
|
+
radio_btn = RadioButtons(
|
|
399
|
+
plt.axes([0.1, 0.05, 0.3, 0.05*len(materials)]),
|
|
400
|
+
labels = list(reversed([m.name for m in materials])),
|
|
401
|
+
activecolor='k')
|
|
402
|
+
radio_cid = radio_btn.on_clicked(modify_material)
|
|
403
|
+
plt.draw()
|
|
404
|
+
|
|
405
|
+
def add(event):
|
|
406
|
+
"""Callback function for the "Add" button."""
|
|
407
|
+
added_material.append(True)
|
|
408
|
+
plt.close()
|
|
409
|
+
|
|
410
|
+
def remove(event):
|
|
411
|
+
"""Callback function for the "Remove" button."""
|
|
412
|
+
for mat_text in mat_texts:
|
|
413
|
+
mat_text.remove()
|
|
414
|
+
mat_texts.clear()
|
|
415
|
+
for button in buttons:
|
|
416
|
+
button[0].disconnect(button[1])
|
|
417
|
+
button[0].ax.remove()
|
|
418
|
+
buttons.clear()
|
|
419
|
+
if len(materials) == 1:
|
|
420
|
+
removed_material.clear()
|
|
421
|
+
removed_material.append(materials[0].name)
|
|
422
|
+
plt.close()
|
|
423
|
+
else:
|
|
424
|
+
def remove_material(label):
|
|
425
|
+
removed_material.clear()
|
|
426
|
+
removed_material.append(label)
|
|
427
|
+
radio_btn.disconnect(radio_cid)
|
|
428
|
+
radio_btn.ax.remove()
|
|
429
|
+
plt.close()
|
|
430
|
+
|
|
431
|
+
mat_texts.append(
|
|
432
|
+
plt.figtext(
|
|
433
|
+
0.1, 0.1 + 0.05*len(materials),
|
|
434
|
+
'Select a material to remove:',
|
|
435
|
+
fontsize='x-large', ha='left', va='center'))
|
|
436
|
+
radio_btn = RadioButtons(
|
|
437
|
+
plt.axes([0.1, 0.05, 0.3, 0.05*len(materials)]),
|
|
438
|
+
labels = list(reversed([m.name for m in materials])),
|
|
439
|
+
activecolor='k')
|
|
440
|
+
removed_material.append(radio_btn.value_selected)
|
|
441
|
+
radio_cid = radio_btn.on_clicked(remove_material)
|
|
442
|
+
plt.draw()
|
|
443
|
+
|
|
444
|
+
def accept(event):
|
|
445
|
+
"""Callback function for the "Accept" button."""
|
|
446
|
+
plt.close()
|
|
447
|
+
|
|
448
|
+
materials = []
|
|
449
|
+
modified_material = []
|
|
450
|
+
added_material = []
|
|
451
|
+
removed_material = []
|
|
452
|
+
mat_texts = []
|
|
453
|
+
buttons = []
|
|
454
|
+
|
|
455
|
+
# Create figure
|
|
456
|
+
fig, ax = plt.subplots(figsize=(11, 8.5))
|
|
457
|
+
ax.set_title(label, fontsize='x-large')
|
|
458
|
+
ax.set_xlabel('Energy (keV)', fontsize='large')
|
|
459
|
+
ax.set_ylabel('Intensity (counts)', fontsize='large')
|
|
460
|
+
ax.set_xlim(x[0], x[-1])
|
|
461
|
+
ax.plot(x, y)
|
|
462
|
+
|
|
463
|
+
# Add materials
|
|
464
|
+
if preselected_materials is None:
|
|
465
|
+
preselected_materials = []
|
|
466
|
+
for m in reversed(preselected_materials):
|
|
467
|
+
add_material(m)
|
|
468
|
+
|
|
469
|
+
# Add materials to figure
|
|
470
|
+
for i, material in enumerate(materials):
|
|
471
|
+
hkls, ds = get_unique_hkls_ds([material])
|
|
472
|
+
E0s = get_peak_locations(ds, tth)
|
|
473
|
+
for hkl, E0 in zip(hkls, E0s):
|
|
474
|
+
if x[0] <= E0 <= x[-1]:
|
|
475
|
+
ax.axvline(E0, c=f'C{i}', ls='--', lw=1)
|
|
476
|
+
ax.text(E0, 1, str(hkl)[1:-1], c=f'C{i}',
|
|
477
|
+
ha='right', va='top', rotation=90,
|
|
478
|
+
transform=ax.get_xaxis_transform())
|
|
479
|
+
|
|
480
|
+
if not interactive:
|
|
481
|
+
|
|
482
|
+
if materials:
|
|
483
|
+
mat_texts.append(
|
|
484
|
+
plt.figtext(
|
|
485
|
+
0.1, 0.05 + 0.05*len(materials),
|
|
486
|
+
'Currently selected materials:',
|
|
487
|
+
fontsize='x-large', ha='left', va='center'))
|
|
488
|
+
plt.subplots_adjust(bottom=0.125 + 0.05*len(materials))
|
|
489
|
+
|
|
490
|
+
else:
|
|
491
|
+
|
|
492
|
+
if materials:
|
|
493
|
+
mat_texts.append(
|
|
494
|
+
plt.figtext(
|
|
495
|
+
0.1, 0.125 + 0.05*len(materials),
|
|
496
|
+
'Currently selected materials:',
|
|
497
|
+
fontsize='x-large', ha='left', va='center'))
|
|
498
|
+
else:
|
|
499
|
+
mat_texts.append(
|
|
500
|
+
plt.figtext(
|
|
501
|
+
0.1, 0.125, 'Add at least one material',
|
|
502
|
+
fontsize='x-large', ha='left', va='center'))
|
|
503
|
+
plt.subplots_adjust(bottom=0.2 + 0.05*len(materials))
|
|
504
|
+
|
|
505
|
+
# Setup "Modify" button
|
|
506
|
+
if materials:
|
|
507
|
+
modify_btn = Button(
|
|
508
|
+
plt.axes([0.1, 0.025, 0.15, 0.05]), 'Modify material')
|
|
509
|
+
modify_cid = modify_btn.on_clicked(modify)
|
|
510
|
+
buttons.append((modify_btn, modify_cid))
|
|
511
|
+
|
|
512
|
+
# Setup "Add" button
|
|
513
|
+
add_btn = Button(plt.axes([0.317, 0.025, 0.15, 0.05]), 'Add material')
|
|
514
|
+
add_cid = add_btn.on_clicked(add)
|
|
515
|
+
buttons.append((add_btn, add_cid))
|
|
516
|
+
|
|
517
|
+
# Setup "Remove" button
|
|
518
|
+
if materials:
|
|
519
|
+
remove_btn = Button(
|
|
520
|
+
plt.axes([0.533, 0.025, 0.15, 0.05]), 'Remove material')
|
|
521
|
+
remove_cid = remove_btn.on_clicked(remove)
|
|
522
|
+
buttons.append((remove_btn, remove_cid))
|
|
523
|
+
|
|
524
|
+
# Setup "Accept" button
|
|
525
|
+
accept_btn = Button(
|
|
526
|
+
plt.axes([0.75, 0.025, 0.15, 0.05]), 'Accept materials')
|
|
527
|
+
accept_cid = accept_btn.on_clicked(accept)
|
|
528
|
+
buttons.append((accept_btn, accept_cid))
|
|
529
|
+
|
|
530
|
+
plt.show()
|
|
531
|
+
|
|
532
|
+
# Disconnect all widget callbacks when figure is closed
|
|
533
|
+
# and remove the buttons before returning the figure
|
|
534
|
+
for button in buttons:
|
|
535
|
+
button[0].disconnect(button[1])
|
|
536
|
+
button[0].ax.remove()
|
|
537
|
+
buttons.clear()
|
|
538
|
+
|
|
539
|
+
if return_buf:
|
|
540
|
+
for mat_text in mat_texts:
|
|
541
|
+
pos = mat_text.get_position()
|
|
542
|
+
if interactive:
|
|
543
|
+
mat_text.set_position((pos[0], pos[1]-0.075))
|
|
544
|
+
else:
|
|
545
|
+
mat_text.set_position(pos)
|
|
546
|
+
if mat_text.get_text() == 'Currently selected materials:':
|
|
547
|
+
mat_text.set_text('Selected materials:')
|
|
548
|
+
mat_text.set_in_layout(True)
|
|
549
|
+
fig.tight_layout(rect=(0, 0.05 + 0.05*len(materials), 1, 1))
|
|
550
|
+
buf = fig_to_iobuf(fig)
|
|
551
|
+
else:
|
|
552
|
+
buf = None
|
|
553
|
+
plt.close()
|
|
554
|
+
|
|
555
|
+
if modified_material:
|
|
556
|
+
# Local modules
|
|
557
|
+
from CHAP.utils.general import input_num_list
|
|
558
|
+
|
|
559
|
+
index = None
|
|
560
|
+
for index, m in enumerate(materials):
|
|
561
|
+
if m.name in modified_material:
|
|
562
|
+
break
|
|
563
|
+
error = True
|
|
564
|
+
while error:
|
|
565
|
+
try:
|
|
566
|
+
print(f'\nCurrent lattice parameters for {m.name}: '
|
|
567
|
+
f'{[m.latticeParameters[i].value for i in range(6)]}')
|
|
568
|
+
lat_params = input_num_list(
|
|
569
|
+
'Enter updated lattice parameters for this material',
|
|
570
|
+
raise_error=True, log=False)
|
|
571
|
+
new_material = MaterialConfig(
|
|
572
|
+
material_name=m.name, sgnum=m.sgnum,
|
|
573
|
+
lattice_parameters=lat_params)
|
|
574
|
+
materials[index] = new_material
|
|
575
|
+
error = False
|
|
576
|
+
except (
|
|
577
|
+
ValueError, TypeError, SyntaxError, MemoryError,
|
|
578
|
+
RecursionError, IndexError) as e:
|
|
579
|
+
print(f'{e}: try again')
|
|
580
|
+
return select_material_params(
|
|
581
|
+
x, y, tth, preselected_materials=materials, label=label,
|
|
582
|
+
interactive=interactive, return_buf=return_buf)
|
|
583
|
+
|
|
584
|
+
if added_material:
|
|
585
|
+
# Local modules
|
|
586
|
+
from CHAP.utils.general import (
|
|
587
|
+
input_int,
|
|
588
|
+
input_num_list,
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
error = True
|
|
592
|
+
while error:
|
|
593
|
+
try:
|
|
594
|
+
print('\nEnter the name of the material to be added:')
|
|
595
|
+
name = input()
|
|
596
|
+
sgnum = input_int(
|
|
597
|
+
'Enter the space group for this material',
|
|
598
|
+
raise_error=True, log=False)
|
|
599
|
+
lat_params = input_num_list(
|
|
600
|
+
'Enter the lattice parameters for this material',
|
|
601
|
+
raise_error=True, log=False)
|
|
602
|
+
print()
|
|
603
|
+
new_material = MaterialConfig(
|
|
604
|
+
material_name=name, sgnum=sgnum,
|
|
605
|
+
lattice_parameters=lat_params)
|
|
606
|
+
error = False
|
|
607
|
+
except (
|
|
608
|
+
ValueError, TypeError, SyntaxError, MemoryError,
|
|
609
|
+
RecursionError, IndexError) as e:
|
|
610
|
+
print(f'{e}: try again')
|
|
611
|
+
materials.append(new_material)
|
|
612
|
+
return select_material_params(
|
|
613
|
+
x, y, tth, preselected_materials=materials, label=label,
|
|
614
|
+
interactive=interactive, return_buf=return_buf)
|
|
615
|
+
|
|
616
|
+
if removed_material:
|
|
617
|
+
return select_material_params(
|
|
618
|
+
x, y, tth,
|
|
619
|
+
preselected_materials=[
|
|
620
|
+
m for m in materials if m.name not in removed_material],
|
|
621
|
+
label=label, interactive=interactive, return_buf=return_buf)
|
|
622
|
+
|
|
623
|
+
if not materials:
|
|
624
|
+
return select_material_params(
|
|
625
|
+
x, y, tth, label=label, interactive=interactive,
|
|
626
|
+
return_buf=return_buf)
|
|
627
|
+
|
|
628
|
+
return [
|
|
629
|
+
MaterialConfig(
|
|
630
|
+
material_name=m.name, sgnum=m.sgnum,
|
|
631
|
+
lattice_parameters=[
|
|
632
|
+
m.latticeParameters[i].value for i in range(6)])
|
|
633
|
+
for m in materials], buf
|
|
634
|
+
|
|
635
|
+
def select_material_params_gui(
|
|
636
|
+
x, y, tth, preselected_materials=None, label='Reference Data',
|
|
637
|
+
interactive=False, return_buf=False):
|
|
638
|
+
"""Interactively adjust the lattice parameters and space group for
|
|
639
|
+
a list of materials. It is possible to add / remove materials from
|
|
640
|
+
the list.
|
|
641
|
+
|
|
642
|
+
:param x: MCA channel energies.
|
|
643
|
+
:type x: np.ndarray
|
|
644
|
+
:param y: MCA intensities.
|
|
645
|
+
:type y: np.ndarray
|
|
646
|
+
:param tth: The (calibrated) 2&theta angle.
|
|
647
|
+
:type tth: float
|
|
648
|
+
:param preselected_materials: Materials to get HKLs and
|
|
649
|
+
lattice spacings for.
|
|
650
|
+
:type preselected_materials: list[hexrd.material.Material],
|
|
651
|
+
optional
|
|
652
|
+
:param label: Legend label for the 1D plot of reference MCA data
|
|
653
|
+
from the parameters `x`, `y`, defaults to `"Reference Data"`.
|
|
654
|
+
:type label: str, optional
|
|
655
|
+
:param interactive: Show the plot and allow user interactions with
|
|
656
|
+
the matplotlib figure, defaults to `False`.
|
|
657
|
+
:type interactive: bool, optional
|
|
658
|
+
:param return_buf: Return an in-memory object as a byte stream
|
|
659
|
+
represention of the Matplotlib figure, defaults to `False`.
|
|
660
|
+
:type return_buf: bool, optional
|
|
661
|
+
:return: The selected materials for the strain analyses and a byte
|
|
662
|
+
stream represention of the Matplotlib figure if return_buf is
|
|
663
|
+
`True` (`None` otherwise).
|
|
664
|
+
:rtype: list[CHAP.edd.models.MaterialConfig],
|
|
665
|
+
Union[io.BytesIO, None]
|
|
666
|
+
"""
|
|
667
|
+
# Local modules
|
|
668
|
+
from CHAP.edd.select_material_params_gui import run_material_selector
|
|
669
|
+
|
|
670
|
+
materials = None
|
|
671
|
+
figure = None
|
|
672
|
+
def on_complete(_materials, _figure):
|
|
673
|
+
nonlocal materials, figure
|
|
674
|
+
materials = _materials
|
|
675
|
+
figure = _figure
|
|
676
|
+
|
|
677
|
+
run_material_selector(
|
|
678
|
+
x, y, tth, preselected_materials, label, on_complete, interactive)
|
|
679
|
+
|
|
680
|
+
if return_buf:
|
|
681
|
+
return materials, fig_to_iobuf(figure)
|
|
682
|
+
return materials, None
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=None,
|
|
686
|
+
preselected_hkl_indices=None, num_hkl_min=1, detector_id=None,
|
|
687
|
+
ref_map=None, flux_energy_range=None, calibration_bin_ranges=None,
|
|
688
|
+
label='Reference Data', interactive=False, return_buf=False):
|
|
689
|
+
"""Return a matplotlib figure to indicate data ranges and HKLs to
|
|
690
|
+
include for fitting in EDD energy/tth calibration and/or strain
|
|
691
|
+
analysis.
|
|
692
|
+
|
|
693
|
+
:param x: MCA channel energies.
|
|
694
|
+
:type x: np.ndarray
|
|
695
|
+
:param y: MCA intensities.
|
|
696
|
+
:type y: np.ndarray
|
|
697
|
+
:param hkls: Avaliable Unique HKL values to fit peaks for in the
|
|
698
|
+
calibration routine.
|
|
699
|
+
:type hkls: list[list[int]]
|
|
700
|
+
:param ds: Lattice spacings associated with the unique HKL indices
|
|
701
|
+
in angstroms.
|
|
702
|
+
:type ds: list[float]
|
|
703
|
+
:param tth: The (calibrated) 2&theta angle.
|
|
704
|
+
:type tth: float
|
|
705
|
+
:param preselected_bin_ranges: Preselected MCA channel index ranges
|
|
706
|
+
whose data should be included after applying a mask.
|
|
707
|
+
:type preselected_bin_ranges: list[list[int]], optional
|
|
708
|
+
:param preselected_hkl_indices: Preselected unique HKL indices to
|
|
709
|
+
fit peaks for in the calibration routine.
|
|
710
|
+
:type preselected_hkl_indices: list[int], optional
|
|
711
|
+
:param num_hkl_min: Minimum number of HKLs to select,
|
|
712
|
+
defaults to `1`.
|
|
713
|
+
:type num_hkl_min: int, optional
|
|
714
|
+
:param detector_id: MCA detector channel index.
|
|
715
|
+
:type detector_id: str, optional
|
|
716
|
+
:param ref_map: Reference map of MCA intensities to show underneath
|
|
717
|
+
the interactive plot.
|
|
718
|
+
:type ref_map: np.ndarray, optional
|
|
719
|
+
:param flux_energy_range: Energy range in eV in the flux file
|
|
720
|
+
containing station beam energy in eV versus flux
|
|
721
|
+
:type flux_energy_range: tuple(float, float), optional
|
|
722
|
+
:param calibration_bin_ranges: MCA channel index ranges included
|
|
723
|
+
in the detector calibration.
|
|
724
|
+
:type calibration_bin_ranges: list[[int, int]], optional
|
|
725
|
+
:param label: Legend label for the 1D plot of reference MCA data
|
|
726
|
+
from the parameters `x`, `y`, defaults to `"Reference Data"`
|
|
727
|
+
:type label: str, optional
|
|
728
|
+
:param interactive: Show the plot and allow user interactions with
|
|
729
|
+
the matplotlib figure, defaults to `True`.
|
|
730
|
+
:type interactive: bool, optional
|
|
731
|
+
:param return_buf: Return an in-memory object as a byte stream
|
|
732
|
+
represention of the Matplotlib figure, defaults to `False`.
|
|
733
|
+
:type return_buf: bool, optional
|
|
734
|
+
:return: The list of selected data index ranges to include, the
|
|
735
|
+
list of HKL indices to include and a byte stream represention
|
|
736
|
+
of the Matplotlib figure if return_buf is `True` (`None`
|
|
737
|
+
otherwise).
|
|
738
|
+
:rtype: list[list[int]], list[int], Union[io.BytesIO, None]
|
|
739
|
+
"""
|
|
740
|
+
# Third party modules
|
|
741
|
+
import matplotlib.lines as mlines
|
|
742
|
+
from matplotlib.patches import Patch
|
|
743
|
+
import matplotlib.pyplot as plt
|
|
744
|
+
from matplotlib.widgets import (
|
|
745
|
+
Button,
|
|
746
|
+
SpanSelector,
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
# Local modules
|
|
750
|
+
from CHAP.utils.general import (
|
|
751
|
+
get_consecutive_int_range,
|
|
752
|
+
index_nearest_down,
|
|
753
|
+
index_nearest_up,
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
def change_fig_title(title):
|
|
757
|
+
"""Change the figure title."""
|
|
758
|
+
if fig_title:
|
|
759
|
+
fig_title[0].remove()
|
|
760
|
+
fig_title.pop()
|
|
761
|
+
fig_title.append(plt.figtext(*title_pos, title, **title_props))
|
|
762
|
+
|
|
763
|
+
def change_error_text(error):
|
|
764
|
+
"""Change the error text."""
|
|
765
|
+
if error_texts:
|
|
766
|
+
error_texts[0].remove()
|
|
767
|
+
error_texts.pop()
|
|
768
|
+
error_texts.append(plt.figtext(*error_pos, error, **error_props))
|
|
769
|
+
|
|
770
|
+
def get_mask():
|
|
771
|
+
"""Return a boolean array that acts as the mask corresponding
|
|
772
|
+
to the currently-selected index ranges.
|
|
773
|
+
"""
|
|
774
|
+
mask = np.full(x.shape[0], False)
|
|
775
|
+
for span in spans:
|
|
776
|
+
_min, _max = span.extents
|
|
777
|
+
mask = np.logical_or(
|
|
778
|
+
mask, np.logical_and(x >= _min, x <= _max))
|
|
779
|
+
return mask
|
|
780
|
+
|
|
781
|
+
def hkl_locations_in_any_span(hkl_index):
|
|
782
|
+
"""Return the index of the span where the location of a specific
|
|
783
|
+
HKL resides. Return(-1 if outside any span.
|
|
784
|
+
"""
|
|
785
|
+
if hkl_index < 0 or hkl_index >= len(hkl_locations):
|
|
786
|
+
return -1
|
|
787
|
+
for i, span in enumerate(spans):
|
|
788
|
+
if (span.extents[0] <= hkl_locations[hkl_index] and
|
|
789
|
+
span.extents[1] >= hkl_locations[hkl_index]):
|
|
790
|
+
return i
|
|
791
|
+
return -1
|
|
792
|
+
|
|
793
|
+
def position_cax():
|
|
794
|
+
"""Reposition the colorbar axes according to the axes of the
|
|
795
|
+
reference map.
|
|
796
|
+
"""
|
|
797
|
+
((_, bottom), (right, top)) = ax_map.get_position().get_points()
|
|
798
|
+
cax.set_position([right + 0.01, bottom, 0.01, top - bottom])
|
|
799
|
+
|
|
800
|
+
def on_span_select(xmin, xmax):
|
|
801
|
+
"""Callback function for the SpanSelector widget."""
|
|
802
|
+
removed_hkls = False
|
|
803
|
+
for hkl_index in deepcopy(selected_hkl_indices):
|
|
804
|
+
if hkl_locations_in_any_span(hkl_index) < 0:
|
|
805
|
+
if interactive or return_buf:
|
|
806
|
+
hkl_vlines[hkl_index].set(**excluded_hkl_props)
|
|
807
|
+
selected_hkl_indices.remove(hkl_index)
|
|
808
|
+
removed_hkls = True
|
|
809
|
+
combined_spans = False
|
|
810
|
+
combined_spans_test = True
|
|
811
|
+
while combined_spans_test:
|
|
812
|
+
combined_spans_test = False
|
|
813
|
+
for i, span1 in enumerate(spans):
|
|
814
|
+
for span2 in reversed(spans[i+1:]):
|
|
815
|
+
if (span1.extents[1] >= span2.extents[0]
|
|
816
|
+
and span1.extents[0] <= span2.extents[1]):
|
|
817
|
+
span1.extents = (
|
|
818
|
+
min(span1.extents[0], span2.extents[0]),
|
|
819
|
+
max(span1.extents[1], span2.extents[1]))
|
|
820
|
+
span2.set_visible(False)
|
|
821
|
+
spans.remove(span2)
|
|
822
|
+
combined_spans = True
|
|
823
|
+
combined_spans_test = True
|
|
824
|
+
break
|
|
825
|
+
if combined_spans_test:
|
|
826
|
+
break
|
|
827
|
+
if flux_energy_range is not None:
|
|
828
|
+
for span in spans:
|
|
829
|
+
min_ = max(span.extents[0], min_x)
|
|
830
|
+
max_ = min(span.extents[1], max_x)
|
|
831
|
+
span.extents = (min_, max_)
|
|
832
|
+
added_hkls = False
|
|
833
|
+
for hkl_index in range(len(hkl_locations)):
|
|
834
|
+
if (hkl_index not in selected_hkl_indices
|
|
835
|
+
and hkl_locations_in_any_span(hkl_index) >= 0):
|
|
836
|
+
if interactive or return_buf:
|
|
837
|
+
hkl_vlines[hkl_index].set(**included_hkl_props)
|
|
838
|
+
selected_hkl_indices.append(hkl_index)
|
|
839
|
+
added_hkls = True
|
|
840
|
+
if interactive or return_buf:
|
|
841
|
+
if combined_spans:
|
|
842
|
+
if added_hkls or removed_hkls:
|
|
843
|
+
change_error_text(
|
|
844
|
+
'Combined overlapping spans and selected only HKL(s) '
|
|
845
|
+
'inside the selected energy mask')
|
|
846
|
+
else:
|
|
847
|
+
change_error_text('Combined overlapping spans in the '
|
|
848
|
+
'selected energy mask')
|
|
849
|
+
elif added_hkls and removed_hkls:
|
|
850
|
+
change_error_text(
|
|
851
|
+
'Adjusted the selected HKL(s) to match the selected '
|
|
852
|
+
'energy mask')
|
|
853
|
+
elif added_hkls:
|
|
854
|
+
change_error_text(
|
|
855
|
+
'Added HKL(s) to match the selected energy mask')
|
|
856
|
+
elif removed_hkls:
|
|
857
|
+
change_error_text(
|
|
858
|
+
'Removed HKL(s) outside the selected energy mask')
|
|
859
|
+
# If using ref_map, update the colorbar range to min / max of
|
|
860
|
+
# the selected data only
|
|
861
|
+
if ref_map is not None:
|
|
862
|
+
selected_data = ref_map[:,get_mask()]
|
|
863
|
+
ref_map_mappable = ax_map.pcolormesh(
|
|
864
|
+
x, np.arange(ref_map.shape[0]), ref_map,
|
|
865
|
+
vmin=selected_data.min(), vmax=selected_data.max())
|
|
866
|
+
fig.colorbar(ref_map_mappable, cax=cax)
|
|
867
|
+
plt.draw()
|
|
868
|
+
|
|
869
|
+
def add_span(event, xrange_init=None):
|
|
870
|
+
"""Callback function for the "Add span" button."""
|
|
871
|
+
spans.append(
|
|
872
|
+
SpanSelector(
|
|
873
|
+
ax, on_span_select, 'horizontal', props=included_data_props,
|
|
874
|
+
useblit=True, interactive=interactive, drag_from_anywhere=True,
|
|
875
|
+
ignore_event_outside=True, grab_range=5))
|
|
876
|
+
if xrange_init is None:
|
|
877
|
+
xmin_init = min_x
|
|
878
|
+
xmax_init = 0.5*(min_x + hkl_locations[0])
|
|
879
|
+
else:
|
|
880
|
+
xmin_init = max(min_x, xrange_init[0])
|
|
881
|
+
xmax_init = min(max_x, xrange_init[1])
|
|
882
|
+
spans[-1]._selection_completed = True
|
|
883
|
+
spans[-1].extents = (xmin_init, xmax_init)
|
|
884
|
+
spans[-1].onselect(xmin_init, xmax_init)
|
|
885
|
+
|
|
886
|
+
if preselected_hkl_indices is None:
|
|
887
|
+
preselected_hkl_indices = []
|
|
888
|
+
selected_hkl_indices = preselected_hkl_indices
|
|
889
|
+
spans = []
|
|
890
|
+
hkl_vlines = []
|
|
891
|
+
fig_title = []
|
|
892
|
+
error_texts = []
|
|
893
|
+
ax_map = cax = None
|
|
894
|
+
|
|
895
|
+
if (ref_map is not None
|
|
896
|
+
and (ref_map.ndim == 1
|
|
897
|
+
or (ref_map.ndim == 2 and ref_map.shape[0] == 1))):
|
|
898
|
+
ref_map = None
|
|
899
|
+
|
|
900
|
+
# Make preselected_bin_ranges consistent with selected_hkl_indices
|
|
901
|
+
if preselected_bin_ranges is None:
|
|
902
|
+
preselected_bin_ranges = []
|
|
903
|
+
hkl_locations = [loc for loc in get_peak_locations(ds, tth)
|
|
904
|
+
if x[0] <= loc <= x[-1]]
|
|
905
|
+
if selected_hkl_indices and not preselected_bin_ranges:
|
|
906
|
+
index_ranges = get_consecutive_int_range(selected_hkl_indices)
|
|
907
|
+
for index_range in index_ranges:
|
|
908
|
+
i = index_range[0]
|
|
909
|
+
if i:
|
|
910
|
+
min_ = 0.5*(hkl_locations[i-1] + hkl_locations[i])
|
|
911
|
+
else:
|
|
912
|
+
min_ = 0.5*(min_x + hkl_locations[i])
|
|
913
|
+
j = index_range[1]
|
|
914
|
+
if j < len(hkl_locations)-1:
|
|
915
|
+
max_ = 0.5*(hkl_locations[j] + hkl_locations[j+1])
|
|
916
|
+
else:
|
|
917
|
+
max_ = 0.5*(hkl_locations[j] + max_x)
|
|
918
|
+
preselected_bin_ranges.append(
|
|
919
|
+
[index_nearest_up(x, min_), index_nearest_down(x, max_)])
|
|
920
|
+
|
|
921
|
+
if flux_energy_range is None:
|
|
922
|
+
min_x = x.min()
|
|
923
|
+
max_x = x.max()
|
|
924
|
+
else:
|
|
925
|
+
min_x = x[index_nearest_up(x, max(x.min(), flux_energy_range[0]))]
|
|
926
|
+
max_x = x[index_nearest_down(x, min(x.max(), flux_energy_range[1]))]
|
|
927
|
+
|
|
928
|
+
# Setup the Matplotlib figure
|
|
929
|
+
title_pos = (0.5, 0.95)
|
|
930
|
+
title_props = {'fontsize': 'xx-large', 'ha': 'center', 'va': 'bottom'}
|
|
931
|
+
error_pos = (0.5, 0.90)
|
|
932
|
+
error_props = {'fontsize': 'x-large', 'ha': 'center', 'va': 'bottom'}
|
|
933
|
+
excluded_hkl_props = {
|
|
934
|
+
'color': 'black', 'linestyle': '--','linewidth': 1,
|
|
935
|
+
'marker': 10, 'markersize': 5, 'fillstyle': 'none'}
|
|
936
|
+
included_hkl_props = {
|
|
937
|
+
'color': 'green', 'linestyle': '-', 'linewidth': 2,
|
|
938
|
+
'marker': 10, 'markersize': 10, 'fillstyle': 'full'}
|
|
939
|
+
if not interactive and not return_buf:
|
|
940
|
+
|
|
941
|
+
# It is too convenient to not use the Matplotlib SpanSelector
|
|
942
|
+
# so define a (fig, ax) tuple, despite not creating a figure
|
|
943
|
+
included_data_props = {}
|
|
944
|
+
fig, ax = plt.subplots()
|
|
945
|
+
|
|
946
|
+
else:
|
|
947
|
+
|
|
948
|
+
excluded_data_props = {
|
|
949
|
+
'facecolor': 'white', 'edgecolor': 'gray', 'linestyle': ':'}
|
|
950
|
+
included_data_props = {
|
|
951
|
+
'alpha': 0.5, 'facecolor': 'tab:blue', 'edgecolor': 'blue'}
|
|
952
|
+
|
|
953
|
+
if ref_map is None:
|
|
954
|
+
fig, ax = plt.subplots(figsize=(11, 8.5))
|
|
955
|
+
ax.set(xlabel='Energy (keV)', ylabel='Intensity (counts)')
|
|
956
|
+
else:
|
|
957
|
+
if ref_map.ndim > 2:
|
|
958
|
+
ref_map = np.reshape(
|
|
959
|
+
ref_map, (np.prod(ref_map.shape[:-1]), ref_map.shape[-1]))
|
|
960
|
+
# If needed, abbreviate ref_map to <= 50 spectra to keep
|
|
961
|
+
# response time of mouse interactions quick.
|
|
962
|
+
max_ref_spectra = 50
|
|
963
|
+
if ref_map.shape[0] > max_ref_spectra:
|
|
964
|
+
choose_i = np.sort(
|
|
965
|
+
np.random.choice(
|
|
966
|
+
ref_map.shape[0], max_ref_spectra, replace=False))
|
|
967
|
+
ref_map = ref_map[choose_i]
|
|
968
|
+
fig, (ax, ax_map) = plt.subplots(
|
|
969
|
+
2, sharex=True, figsize=(11, 8.5), height_ratios=[2, 1])
|
|
970
|
+
ax.set(ylabel='Intensity (counts)')
|
|
971
|
+
ref_map_mappable = ax_map.pcolormesh(
|
|
972
|
+
x, np.arange(ref_map.shape[0]), ref_map)
|
|
973
|
+
ax_map.set_yticks([])
|
|
974
|
+
ax_map.set_xlabel('Energy (keV)')
|
|
975
|
+
ax_map.set_xlim(x[0], x[-1])
|
|
976
|
+
((_, bottom), (right, top)) = ax_map.get_position().get_points()
|
|
977
|
+
cax = plt.axes([right + 0.01, bottom, 0.01, top - bottom])
|
|
978
|
+
fig.colorbar(ref_map_mappable, cax=cax)
|
|
979
|
+
handles = ax.plot(x, y, color='k', label=label)
|
|
980
|
+
if calibration_bin_ranges is not None:
|
|
981
|
+
ylow = ax.get_ylim()[0]
|
|
982
|
+
for low, upp in calibration_bin_ranges:
|
|
983
|
+
ax.plot([x[low], x[upp]], [ylow, ylow], color='r', linewidth=2)
|
|
984
|
+
handles.append(mlines.Line2D(
|
|
985
|
+
[], [], label='Energies included in calibration', color='r',
|
|
986
|
+
linewidth=2))
|
|
987
|
+
handles.append(mlines.Line2D(
|
|
988
|
+
[], [], label='Excluded / unselected HKL', **excluded_hkl_props))
|
|
989
|
+
handles.append(mlines.Line2D(
|
|
990
|
+
[], [], label='Included / selected HKL', **included_hkl_props))
|
|
991
|
+
handles.append(Patch(
|
|
992
|
+
label='Excluded / unselected data', **excluded_data_props))
|
|
993
|
+
handles.append(Patch(
|
|
994
|
+
label='Included / selected data', **included_data_props))
|
|
995
|
+
ax.legend(handles=handles)
|
|
996
|
+
ax.set_xlim(x[0], x[-1])
|
|
997
|
+
|
|
998
|
+
# Add HKL lines
|
|
999
|
+
hkl_labels = [str(hkl)[1:-1] for hkl, loc in zip(hkls, hkl_locations)]
|
|
1000
|
+
for i, (loc, lbl) in enumerate(zip(hkl_locations, hkl_labels)):
|
|
1001
|
+
if i in selected_hkl_indices:
|
|
1002
|
+
hkl_vline = ax.axvline(loc, **included_hkl_props)
|
|
1003
|
+
else:
|
|
1004
|
+
hkl_vline = ax.axvline(loc, **excluded_hkl_props)
|
|
1005
|
+
ax.text(loc, 1, lbl, ha='right', va='top', rotation=90,
|
|
1006
|
+
transform=ax.get_xaxis_transform())
|
|
1007
|
+
hkl_vlines.append(hkl_vline)
|
|
1008
|
+
|
|
1009
|
+
# Add initial spans
|
|
1010
|
+
for bin_range in preselected_bin_ranges:
|
|
1011
|
+
add_span(None, xrange_init=x[bin_range])
|
|
1012
|
+
|
|
1013
|
+
if not interactive:
|
|
1014
|
+
|
|
1015
|
+
if return_buf:
|
|
1016
|
+
if detector_id is None:
|
|
1017
|
+
change_fig_title('Selected data and HKLs used in fitting')
|
|
1018
|
+
else:
|
|
1019
|
+
change_fig_title('Selected data and HKLs used in fitting '
|
|
1020
|
+
f'detector {detector_id}')
|
|
1021
|
+
if error_texts:
|
|
1022
|
+
error_texts[0].remove()
|
|
1023
|
+
error_texts.pop()
|
|
1024
|
+
|
|
1025
|
+
else:
|
|
1026
|
+
|
|
1027
|
+
def pick_hkl(event):
|
|
1028
|
+
"""The "onpick" callback function."""
|
|
1029
|
+
try:
|
|
1030
|
+
hkl_index = hkl_vlines.index(event.artist)
|
|
1031
|
+
except Exception:
|
|
1032
|
+
pass
|
|
1033
|
+
else:
|
|
1034
|
+
hkl_vline = event.artist
|
|
1035
|
+
if hkl_index in deepcopy(selected_hkl_indices):
|
|
1036
|
+
hkl_vline.set(**excluded_hkl_props)
|
|
1037
|
+
selected_hkl_indices.remove(hkl_index)
|
|
1038
|
+
span = spans[hkl_locations_in_any_span(hkl_index)]
|
|
1039
|
+
span_p_hkl_index = hkl_locations_in_any_span(hkl_index-1)
|
|
1040
|
+
span_c_hkl_index = hkl_locations_in_any_span(hkl_index)
|
|
1041
|
+
span_n_hkl_index = hkl_locations_in_any_span(hkl_index+1)
|
|
1042
|
+
if span_c_hkl_index not in (span_p_hkl_index,
|
|
1043
|
+
span_n_hkl_index):
|
|
1044
|
+
span.set_visible(False)
|
|
1045
|
+
spans.remove(span)
|
|
1046
|
+
elif span_c_hkl_index != span_n_hkl_index:
|
|
1047
|
+
span.extents = (
|
|
1048
|
+
span.extents[0],
|
|
1049
|
+
0.5*(hkl_locations[hkl_index-1]
|
|
1050
|
+
+ hkl_locations[hkl_index]))
|
|
1051
|
+
elif span_c_hkl_index != span_p_hkl_index:
|
|
1052
|
+
span.extents = (
|
|
1053
|
+
0.5*(hkl_locations[hkl_index]
|
|
1054
|
+
+ hkl_locations[hkl_index+1]),
|
|
1055
|
+
span.extents[1])
|
|
1056
|
+
else:
|
|
1057
|
+
xrange_init = [
|
|
1058
|
+
0.5*(hkl_locations[hkl_index]
|
|
1059
|
+
+ hkl_locations[hkl_index+1]),
|
|
1060
|
+
span.extents[1]]
|
|
1061
|
+
span.extents = (
|
|
1062
|
+
span.extents[0],
|
|
1063
|
+
0.5*(hkl_locations[hkl_index-1]
|
|
1064
|
+
+ hkl_locations[hkl_index]))
|
|
1065
|
+
add_span(None, xrange_init=xrange_init)
|
|
1066
|
+
change_error_text(
|
|
1067
|
+
'Adjusted the selected energy mask to reflect the '
|
|
1068
|
+
'removed HKL')
|
|
1069
|
+
else:
|
|
1070
|
+
hkl_vline.set(**included_hkl_props)
|
|
1071
|
+
p_hkl = hkl_index-1 in selected_hkl_indices
|
|
1072
|
+
n_hkl = hkl_index+1 in selected_hkl_indices
|
|
1073
|
+
if p_hkl and n_hkl:
|
|
1074
|
+
span_p = spans[hkl_locations_in_any_span(hkl_index-1)]
|
|
1075
|
+
span_n = spans[hkl_locations_in_any_span(hkl_index+1)]
|
|
1076
|
+
span_p.extents = (
|
|
1077
|
+
span_p.extents[0], span_n.extents[1])
|
|
1078
|
+
span_n.set_visible(False)
|
|
1079
|
+
elif p_hkl:
|
|
1080
|
+
span_p = spans[hkl_locations_in_any_span(hkl_index-1)]
|
|
1081
|
+
if hkl_index < len(hkl_locations)-1:
|
|
1082
|
+
max_ = 0.5*(
|
|
1083
|
+
hkl_locations[hkl_index]
|
|
1084
|
+
+ hkl_locations[hkl_index+1])
|
|
1085
|
+
else:
|
|
1086
|
+
max_ = 0.5*(hkl_locations[hkl_index] + max_x)
|
|
1087
|
+
span_p.extents = (span_p.extents[0], max_)
|
|
1088
|
+
elif n_hkl:
|
|
1089
|
+
span_n = spans[hkl_locations_in_any_span(hkl_index+1)]
|
|
1090
|
+
if hkl_index > 0:
|
|
1091
|
+
min_ = 0.5*(
|
|
1092
|
+
hkl_locations[hkl_index-1]
|
|
1093
|
+
+ hkl_locations[hkl_index])
|
|
1094
|
+
else:
|
|
1095
|
+
min_ = 0.5*(min_x + hkl_locations[hkl_index])
|
|
1096
|
+
span_n.extents = (min_, span_n.extents[1])
|
|
1097
|
+
else:
|
|
1098
|
+
if hkl_index > 0:
|
|
1099
|
+
min_ = 0.5*(
|
|
1100
|
+
hkl_locations[hkl_index-1]
|
|
1101
|
+
+ hkl_locations[hkl_index])
|
|
1102
|
+
else:
|
|
1103
|
+
min_ = 0.5*(min_x + hkl_locations[hkl_index])
|
|
1104
|
+
if hkl_index < len(hkl_locations)-1:
|
|
1105
|
+
max_ = 0.5*(
|
|
1106
|
+
hkl_locations[hkl_index]
|
|
1107
|
+
+ hkl_locations[hkl_index+1])
|
|
1108
|
+
else:
|
|
1109
|
+
max_ = 0.5*(hkl_locations[hkl_index] + max_x)
|
|
1110
|
+
add_span(None, xrange_init=(min_, max_))
|
|
1111
|
+
change_error_text(
|
|
1112
|
+
'Adjusted the selected energy mask to reflect the '
|
|
1113
|
+
'added HKL')
|
|
1114
|
+
plt.draw()
|
|
1115
|
+
|
|
1116
|
+
def reset(event):
|
|
1117
|
+
"""Callback function for the "Reset" button."""
|
|
1118
|
+
for hkl_index in deepcopy(selected_hkl_indices):
|
|
1119
|
+
hkl_vlines[hkl_index].set(**excluded_hkl_props)
|
|
1120
|
+
selected_hkl_indices.remove(hkl_index)
|
|
1121
|
+
for span in reversed(spans):
|
|
1122
|
+
span.set_visible(False)
|
|
1123
|
+
spans.remove(span)
|
|
1124
|
+
plt.draw()
|
|
1125
|
+
|
|
1126
|
+
def confirm(event):
|
|
1127
|
+
"""Callback function for the "Confirm" button."""
|
|
1128
|
+
if not spans or len(selected_hkl_indices) < num_hkl_min:
|
|
1129
|
+
change_error_text(
|
|
1130
|
+
f'Select at least one span and {num_hkl_min} HKLs')
|
|
1131
|
+
plt.draw()
|
|
1132
|
+
else:
|
|
1133
|
+
if error_texts:
|
|
1134
|
+
error_texts[0].remove()
|
|
1135
|
+
error_texts.pop()
|
|
1136
|
+
if detector_id is None:
|
|
1137
|
+
change_fig_title('Selected data and HKLs used in fitting')
|
|
1138
|
+
else:
|
|
1139
|
+
change_fig_title('Selected data and HKLs used in fitting '
|
|
1140
|
+
f'detector {detector_id}')
|
|
1141
|
+
plt.close()
|
|
1142
|
+
|
|
1143
|
+
if detector_id is None:
|
|
1144
|
+
change_fig_title('Select data and HKLs to use in fitting')
|
|
1145
|
+
else:
|
|
1146
|
+
change_fig_title('Select data and HKLs to use in fitting '
|
|
1147
|
+
f'detector {detector_id}')
|
|
1148
|
+
fig.subplots_adjust(bottom=0.2)
|
|
1149
|
+
if not return_buf and ref_map is not None:
|
|
1150
|
+
position_cax()
|
|
1151
|
+
|
|
1152
|
+
# Setup "Add span" button
|
|
1153
|
+
add_span_btn = Button(plt.axes([0.125, 0.05, 0.15, 0.075]), 'Add span')
|
|
1154
|
+
add_span_cid = add_span_btn.on_clicked(add_span)
|
|
1155
|
+
|
|
1156
|
+
for vline in hkl_vlines:
|
|
1157
|
+
vline.set_picker(5)
|
|
1158
|
+
pick_hkl_cid = fig.canvas.mpl_connect('pick_event', pick_hkl)
|
|
1159
|
+
|
|
1160
|
+
# Setup "Reset" button
|
|
1161
|
+
reset_btn = Button(plt.axes([0.4375, 0.05, 0.15, 0.075]), 'Reset')
|
|
1162
|
+
reset_cid = reset_btn.on_clicked(reset)
|
|
1163
|
+
|
|
1164
|
+
# Setup "Confirm" button
|
|
1165
|
+
confirm_btn = Button(plt.axes([0.75, 0.05, 0.15, 0.075]), 'Confirm')
|
|
1166
|
+
confirm_cid = confirm_btn.on_clicked(confirm)
|
|
1167
|
+
|
|
1168
|
+
# Show figure for user interaction
|
|
1169
|
+
plt.show()
|
|
1170
|
+
|
|
1171
|
+
# Disconnect all widget callbacks when figure is closed
|
|
1172
|
+
add_span_btn.disconnect(add_span_cid)
|
|
1173
|
+
fig.canvas.mpl_disconnect(pick_hkl_cid)
|
|
1174
|
+
reset_btn.disconnect(reset_cid)
|
|
1175
|
+
confirm_btn.disconnect(confirm_cid)
|
|
1176
|
+
|
|
1177
|
+
# ...and remove the buttons before returning the figure
|
|
1178
|
+
add_span_btn.ax.remove()
|
|
1179
|
+
confirm_btn.ax.remove()
|
|
1180
|
+
reset_btn.ax.remove()
|
|
1181
|
+
plt.subplots_adjust(bottom=0.0)
|
|
1182
|
+
|
|
1183
|
+
if return_buf:
|
|
1184
|
+
if interactive:
|
|
1185
|
+
if error_texts:
|
|
1186
|
+
error_texts[0].remove()
|
|
1187
|
+
error_texts.pop()
|
|
1188
|
+
title = 'Selected data and HKLs used in fitting'
|
|
1189
|
+
if detector_id is not None:
|
|
1190
|
+
title += f' detector {detector_id}'
|
|
1191
|
+
fig_title[0]._text = title
|
|
1192
|
+
fig_title[0].set_in_layout(True)
|
|
1193
|
+
fig.tight_layout(rect=(0, 0, 0.9, 0.9))
|
|
1194
|
+
if ref_map is not None:
|
|
1195
|
+
position_cax()
|
|
1196
|
+
buf = fig_to_iobuf(fig)
|
|
1197
|
+
else:
|
|
1198
|
+
buf = None
|
|
1199
|
+
plt.close()
|
|
1200
|
+
|
|
1201
|
+
selected_bin_ranges = [np.searchsorted(x, span.extents).tolist()
|
|
1202
|
+
for span in spans]
|
|
1203
|
+
if not selected_bin_ranges:
|
|
1204
|
+
selected_bin_ranges = None
|
|
1205
|
+
if selected_hkl_indices:
|
|
1206
|
+
selected_hkl_indices = sorted(selected_hkl_indices)
|
|
1207
|
+
else:
|
|
1208
|
+
selected_hkl_indices = None
|
|
1209
|
+
|
|
1210
|
+
return selected_bin_ranges, selected_hkl_indices, buf
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
def get_rolling_sum_spectra(
|
|
1214
|
+
y, bin_axis, start=0, end=None, width=None, stride=None, num=None,
|
|
1215
|
+
mode='valid'):
|
|
1216
|
+
"""Return the rolling sum of the spectra over a specified axis."""
|
|
1217
|
+
y = np.asarray(y)
|
|
1218
|
+
if not 0 <= bin_axis < y.ndim-1:
|
|
1219
|
+
raise ValueError(f'Invalid "bin_axis" parameter ({bin_axis})')
|
|
1220
|
+
size = y.shape[bin_axis]
|
|
1221
|
+
if not 0 <= start < size:
|
|
1222
|
+
raise ValueError(f'Invalid "start" parameter ({start})')
|
|
1223
|
+
if end is None:
|
|
1224
|
+
end = size
|
|
1225
|
+
elif not start < end <= size:
|
|
1226
|
+
raise ValueError('Invalid "start" and "end" combination '
|
|
1227
|
+
f'({start} and {end})')
|
|
1228
|
+
|
|
1229
|
+
size = end-start
|
|
1230
|
+
if stride is None:
|
|
1231
|
+
if width is None:
|
|
1232
|
+
width = max(1, int(size/num))
|
|
1233
|
+
stride = width
|
|
1234
|
+
else:
|
|
1235
|
+
width = max(1, min(width, size))
|
|
1236
|
+
if num is None:
|
|
1237
|
+
stride = width
|
|
1238
|
+
else:
|
|
1239
|
+
stride = max(1, int((size-width) / (num-1)))
|
|
1240
|
+
else:
|
|
1241
|
+
stride = max(1, min(stride, size-stride))
|
|
1242
|
+
if width is None:
|
|
1243
|
+
width = stride
|
|
1244
|
+
if mode == 'valid':
|
|
1245
|
+
num = 1 + max(0, int((size-width) / stride))
|
|
1246
|
+
else:
|
|
1247
|
+
num = int(size/stride)
|
|
1248
|
+
if num*stride < size:
|
|
1249
|
+
num += 1
|
|
1250
|
+
bin_ranges = [(start+n*stride, min(start+size, start+n*stride+width))
|
|
1251
|
+
for n in range(num)]
|
|
1252
|
+
|
|
1253
|
+
y_shape = y.shape
|
|
1254
|
+
y_ndim = y.ndim
|
|
1255
|
+
swap_axis = False
|
|
1256
|
+
if y_ndim > 2 and bin_axis != y_ndim-2:
|
|
1257
|
+
y = np.swapaxes(y, bin_axis, y_ndim-2)
|
|
1258
|
+
swap_axis = True
|
|
1259
|
+
if y_ndim > 3:
|
|
1260
|
+
map_shape = y.shape[0:y_ndim-2]
|
|
1261
|
+
y = y.reshape((np.prod(map_shape), *y.shape[y_ndim-2:]))
|
|
1262
|
+
if y_ndim == 2:
|
|
1263
|
+
y = np.expand_dims(y, 0)
|
|
1264
|
+
|
|
1265
|
+
ry = np.zeros((y.shape[0], num, y.shape[-1]), dtype=y.dtype)
|
|
1266
|
+
for dim in range(y.shape[0]):
|
|
1267
|
+
for n in range(num):
|
|
1268
|
+
ry[dim, n] = np.sum(y[dim,bin_ranges[n][0]:bin_ranges[n][1]], 0)
|
|
1269
|
+
|
|
1270
|
+
if y_ndim > 3:
|
|
1271
|
+
ry = np.reshape(ry, (*map_shape, num, y_shape[-1]))
|
|
1272
|
+
if y_ndim == 2:
|
|
1273
|
+
ry = np.squeeze(ry)
|
|
1274
|
+
if swap_axis:
|
|
1275
|
+
ry = np.swapaxes(ry, bin_axis, y_ndim-2)
|
|
1276
|
+
|
|
1277
|
+
return ry
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
def get_spectra_fits(
|
|
1281
|
+
spectra, energies, peak_locations, detector, **kwargs):
|
|
1282
|
+
"""Return twenty arrays of fit results for the map of spectra
|
|
1283
|
+
provided: uniform centers, uniform center errors, uniform
|
|
1284
|
+
amplitudes, uniform amplitude errors, uniform sigmas, uniform
|
|
1285
|
+
sigma errors, uniform best fit, uniform residuals, uniform reduced
|
|
1286
|
+
chi, uniform success codes, unconstrained centers, unconstrained
|
|
1287
|
+
center errors, unconstrained amplitudes, unconstrained amplitude
|
|
1288
|
+
errors, unconstrained sigmas, unconstrained sigma errors,
|
|
1289
|
+
unconstrained best fit, unconstrained residuals, unconstrained
|
|
1290
|
+
reduced chi, and unconstrained success codes.
|
|
1291
|
+
|
|
1292
|
+
:param spectra: Array of intensity spectra to fit.
|
|
1293
|
+
:type spectra: numpy.ndarray
|
|
1294
|
+
:param energies: Bin energies for the spectra provided.
|
|
1295
|
+
:type energies: numpy.ndarray
|
|
1296
|
+
:param peak_locations: Initial guesses for peak ceneters to use
|
|
1297
|
+
for the uniform fit.
|
|
1298
|
+
:type peak_locations: list[float]
|
|
1299
|
+
:param detector: A single MCA detector element configuration.
|
|
1300
|
+
:type detector: CHAP.edd.models.MCAElementStrainAnalysisConfig
|
|
1301
|
+
:returns: Uniform and unconstrained centers, amplitdues, sigmas
|
|
1302
|
+
(and errors for all three), best fits, residuals between the
|
|
1303
|
+
best fits and the input spectra, reduced chi, and fit success
|
|
1304
|
+
statuses.
|
|
1305
|
+
:rtype: tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray,
|
|
1306
|
+
numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray,
|
|
1307
|
+
numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray,
|
|
1308
|
+
numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray,
|
|
1309
|
+
numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray,
|
|
1310
|
+
numpy.ndarray]
|
|
1311
|
+
"""
|
|
1312
|
+
# System modules
|
|
1313
|
+
from os import getpid
|
|
1314
|
+
|
|
1315
|
+
# Third party modules
|
|
1316
|
+
from nexusformat.nexus import (
|
|
1317
|
+
NXdata,
|
|
1318
|
+
NXfield,
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
# Local modules
|
|
1322
|
+
from CHAP.utils.fit import FitProcessor
|
|
1323
|
+
|
|
1324
|
+
num_proc = kwargs.get('num_proc', 1)
|
|
1325
|
+
rel_height_cutoff = detector.rel_height_cutoff
|
|
1326
|
+
num_peak = len(peak_locations)
|
|
1327
|
+
nxdata = NXdata(NXfield(spectra, 'y'), NXfield(energies, 'x'))
|
|
1328
|
+
|
|
1329
|
+
# Construct the fit model
|
|
1330
|
+
models = []
|
|
1331
|
+
if detector.background is not None:
|
|
1332
|
+
if isinstance(detector.background, str):
|
|
1333
|
+
models.append(
|
|
1334
|
+
{'model': detector.background, 'prefix': 'bkgd_'})
|
|
1335
|
+
else:
|
|
1336
|
+
for model in detector.background:
|
|
1337
|
+
models.append({'model': model, 'prefix': f'{model}_'})
|
|
1338
|
+
if detector.backgroundpeaks is not None:
|
|
1339
|
+
_, backgroundpeaks = FitProcessor.create_multipeak_model(
|
|
1340
|
+
detector.backgroundpeaks)
|
|
1341
|
+
for peak in backgroundpeaks:
|
|
1342
|
+
peak.prefix = f'bkgd_{peak.prefix}'
|
|
1343
|
+
models += backgroundpeaks
|
|
1344
|
+
models.append(
|
|
1345
|
+
{'model': 'multipeak', 'centers': list(peak_locations),
|
|
1346
|
+
'fit_type': 'uniform', 'peak_models': detector.peak_models,
|
|
1347
|
+
'centers_range': detector.centers_range,
|
|
1348
|
+
'fwhm_min': detector.fwhm_min, 'fwhm_max': detector.fwhm_max})
|
|
1349
|
+
config = {
|
|
1350
|
+
# 'code': 'lmfit',
|
|
1351
|
+
'models': models,
|
|
1352
|
+
# 'plot': True,
|
|
1353
|
+
'num_proc': num_proc,
|
|
1354
|
+
'rel_height_cutoff': rel_height_cutoff,
|
|
1355
|
+
# 'method': 'trf',
|
|
1356
|
+
'method': 'leastsq',
|
|
1357
|
+
# 'method': 'least_squares',
|
|
1358
|
+
'memfolder': f'/tmp/{getpid()}_joblib_memmap',
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
# Perform uniform fit
|
|
1362
|
+
fit = FitProcessor(**kwargs)
|
|
1363
|
+
uniform_fit = fit.process(nxdata, config)
|
|
1364
|
+
uniform_success = uniform_fit.success
|
|
1365
|
+
if spectra.ndim == 1:
|
|
1366
|
+
if uniform_success:
|
|
1367
|
+
if num_peak == 1:
|
|
1368
|
+
uniform_fit_centers = [uniform_fit.best_values['center']]
|
|
1369
|
+
uniform_fit_centers_errors = [
|
|
1370
|
+
uniform_fit.best_errors['center']]
|
|
1371
|
+
uniform_fit_amplitudes = [
|
|
1372
|
+
uniform_fit.best_values['amplitude']]
|
|
1373
|
+
uniform_fit_amplitudes_errors = [
|
|
1374
|
+
uniform_fit.best_errors['amplitude']]
|
|
1375
|
+
uniform_fit_sigmas = [uniform_fit.best_values['sigma']]
|
|
1376
|
+
uniform_fit_centers_errors = [uniform_fit.best_errors['sigma']]
|
|
1377
|
+
else:
|
|
1378
|
+
uniform_fit_centers = [
|
|
1379
|
+
uniform_fit.best_values[
|
|
1380
|
+
f'peak{i+1}_center'] for i in range(num_peak)]
|
|
1381
|
+
uniform_fit_centers_errors = [
|
|
1382
|
+
uniform_fit.best_errors[
|
|
1383
|
+
f'peak{i+1}_center'] for i in range(num_peak)]
|
|
1384
|
+
uniform_fit_amplitudes = [
|
|
1385
|
+
uniform_fit.best_values[
|
|
1386
|
+
f'peak{i+1}_amplitude'] for i in range(num_peak)]
|
|
1387
|
+
uniform_fit_amplitudes_errors = [
|
|
1388
|
+
uniform_fit.best_errors[
|
|
1389
|
+
f'peak{i+1}_amplitude'] for i in range(num_peak)]
|
|
1390
|
+
uniform_fit_sigmas = [
|
|
1391
|
+
uniform_fit.best_values[
|
|
1392
|
+
f'peak{i+1}_sigma'] for i in range(num_peak)]
|
|
1393
|
+
uniform_fit_sigmas_errors = [
|
|
1394
|
+
uniform_fit.best_errors[
|
|
1395
|
+
f'peak{i+1}_sigma'] for i in range(num_peak)]
|
|
1396
|
+
else:
|
|
1397
|
+
uniform_fit_centers = list(peak_locations)
|
|
1398
|
+
uniform_fit_centers_errors = [0]
|
|
1399
|
+
uniform_fit_amplitudes = [0]
|
|
1400
|
+
uniform_fit_amplitudes_errors = [0]
|
|
1401
|
+
uniform_fit_sigmas = [0]
|
|
1402
|
+
uniform_fit_sigmas_errors = [0]
|
|
1403
|
+
else:
|
|
1404
|
+
if num_peak == 1:
|
|
1405
|
+
uniform_fit_centers = [
|
|
1406
|
+
uniform_fit.best_values[
|
|
1407
|
+
uniform_fit.best_parameters().index('center')]]
|
|
1408
|
+
uniform_fit_centers_errors = [
|
|
1409
|
+
uniform_fit.best_errors[
|
|
1410
|
+
uniform_fit.best_parameters().index('center')]]
|
|
1411
|
+
uniform_fit_amplitudes = [
|
|
1412
|
+
uniform_fit.best_values[
|
|
1413
|
+
uniform_fit.best_parameters().index('amplitude')]]
|
|
1414
|
+
uniform_fit_amplitudes_errors = [
|
|
1415
|
+
uniform_fit.best_errors[
|
|
1416
|
+
uniform_fit.best_parameters().index('amplitude')]]
|
|
1417
|
+
uniform_fit_sigmas = [
|
|
1418
|
+
uniform_fit.best_values[
|
|
1419
|
+
uniform_fit.best_parameters().index('sigma')]]
|
|
1420
|
+
uniform_fit_sigmas_errors = [
|
|
1421
|
+
uniform_fit.best_errors[
|
|
1422
|
+
uniform_fit.best_parameters().index('sigma')]]
|
|
1423
|
+
else:
|
|
1424
|
+
uniform_fit_centers = [
|
|
1425
|
+
uniform_fit.best_values[
|
|
1426
|
+
uniform_fit.best_parameters().index(f'peak{i+1}_center')]
|
|
1427
|
+
for i in range(num_peak)]
|
|
1428
|
+
uniform_fit_centers_errors = [
|
|
1429
|
+
uniform_fit.best_errors[
|
|
1430
|
+
uniform_fit.best_parameters().index(f'peak{i+1}_center')]
|
|
1431
|
+
for i in range(num_peak)]
|
|
1432
|
+
uniform_fit_amplitudes = [
|
|
1433
|
+
uniform_fit.best_values[
|
|
1434
|
+
uniform_fit.best_parameters().index(
|
|
1435
|
+
f'peak{i+1}_amplitude')]
|
|
1436
|
+
for i in range(num_peak)]
|
|
1437
|
+
uniform_fit_amplitudes_errors = [
|
|
1438
|
+
uniform_fit.best_errors[
|
|
1439
|
+
uniform_fit.best_parameters().index(
|
|
1440
|
+
f'peak{i+1}_amplitude')]
|
|
1441
|
+
for i in range(num_peak)]
|
|
1442
|
+
uniform_fit_sigmas = [
|
|
1443
|
+
uniform_fit.best_values[
|
|
1444
|
+
uniform_fit.best_parameters().index(f'peak{i+1}_sigma')]
|
|
1445
|
+
for i in range(num_peak)]
|
|
1446
|
+
uniform_fit_sigmas_errors = [
|
|
1447
|
+
uniform_fit.best_errors[
|
|
1448
|
+
uniform_fit.best_parameters().index(f'peak{i+1}_sigma')]
|
|
1449
|
+
for i in range(num_peak)]
|
|
1450
|
+
if not np.asarray(uniform_success).all():
|
|
1451
|
+
for n in range(num_peak):
|
|
1452
|
+
uniform_fit_centers[n] = np.where(
|
|
1453
|
+
uniform_success, uniform_fit_centers[n], peak_locations[n])
|
|
1454
|
+
uniform_fit_centers_errors[n] *= uniform_success
|
|
1455
|
+
uniform_fit_amplitudes[n] *= uniform_success
|
|
1456
|
+
uniform_fit_amplitudes_errors[n] *= uniform_success
|
|
1457
|
+
uniform_fit_sigmas[n] *= uniform_success
|
|
1458
|
+
uniform_fit_sigmas_errors[n] *= uniform_success
|
|
1459
|
+
|
|
1460
|
+
if num_peak == 1:
|
|
1461
|
+
return (
|
|
1462
|
+
{'centers': uniform_fit_centers,
|
|
1463
|
+
'centers_errors': uniform_fit_centers_errors,
|
|
1464
|
+
'amplitudes': uniform_fit_amplitudes,
|
|
1465
|
+
'amplitudes_errors': uniform_fit_amplitudes_errors,
|
|
1466
|
+
'sigmas': uniform_fit_sigmas,
|
|
1467
|
+
'sigmas_errors': uniform_fit_sigmas_errors,
|
|
1468
|
+
'best_fits': uniform_fit.best_fit,
|
|
1469
|
+
'residuals': uniform_fit.residual,
|
|
1470
|
+
'redchis': uniform_fit.redchi,
|
|
1471
|
+
'success': uniform_success},
|
|
1472
|
+
{'centers': uniform_fit_centers,
|
|
1473
|
+
'centers_errors': uniform_fit_centers_errors,
|
|
1474
|
+
'amplitudes': uniform_fit_amplitudes,
|
|
1475
|
+
'amplitudes_errors': uniform_fit_amplitudes_errors,
|
|
1476
|
+
'sigmas': uniform_fit_sigmas,
|
|
1477
|
+
'sigmas_errors': uniform_fit_sigmas_errors,
|
|
1478
|
+
'best_fits': uniform_fit.best_fit,
|
|
1479
|
+
'residuals': uniform_fit.residual,
|
|
1480
|
+
'redchis': uniform_fit.redchi,
|
|
1481
|
+
'success': uniform_success})
|
|
1482
|
+
|
|
1483
|
+
# Perform unconstrained fit
|
|
1484
|
+
config['models'][-1]['fit_type'] = 'unconstrained'
|
|
1485
|
+
unconstrained_fit = fit.process(uniform_fit, config)
|
|
1486
|
+
unconstrained_success = unconstrained_fit.success
|
|
1487
|
+
if spectra.ndim == 1:
|
|
1488
|
+
if unconstrained_success:
|
|
1489
|
+
unconstrained_fit_centers = [
|
|
1490
|
+
unconstrained_fit.best_values[
|
|
1491
|
+
f'peak{i+1}_center'] for i in range(num_peak)]
|
|
1492
|
+
unconstrained_fit_centers_errors = [
|
|
1493
|
+
unconstrained_fit.best_errors[
|
|
1494
|
+
f'peak{i+1}_center'] for i in range(num_peak)]
|
|
1495
|
+
unconstrained_fit_amplitudes = [
|
|
1496
|
+
unconstrained_fit.best_values[
|
|
1497
|
+
f'peak{i+1}_amplitude'] for i in range(num_peak)]
|
|
1498
|
+
unconstrained_fit_amplitudes_errors = [
|
|
1499
|
+
unconstrained_fit.best_errors[
|
|
1500
|
+
f'peak{i+1}_amplitude'] for i in range(num_peak)]
|
|
1501
|
+
unconstrained_fit_sigmas = [
|
|
1502
|
+
unconstrained_fit.best_values[
|
|
1503
|
+
f'peak{i+1}_sigma'] for i in range(num_peak)]
|
|
1504
|
+
unconstrained_fit_sigmas_errors = [
|
|
1505
|
+
unconstrained_fit.best_errors[
|
|
1506
|
+
f'peak{i+1}_sigma'] for i in range(num_peak)]
|
|
1507
|
+
else:
|
|
1508
|
+
unconstrained_fit_centers = list(peak_locations)
|
|
1509
|
+
unconstrained_fit_centers_errors = [0]
|
|
1510
|
+
unconstrained_fit_amplitudes = [0]
|
|
1511
|
+
unconstrained_fit_amplitudes_errors = [0]
|
|
1512
|
+
unconstrained_fit_sigmas = [0]
|
|
1513
|
+
unconstrained_fit_sigmas_errors = [0]
|
|
1514
|
+
else:
|
|
1515
|
+
unconstrained_fit_centers = np.array(
|
|
1516
|
+
[unconstrained_fit.best_values[
|
|
1517
|
+
unconstrained_fit.best_parameters().index(f'peak{i+1}_center')]
|
|
1518
|
+
for i in range(num_peak)])
|
|
1519
|
+
unconstrained_fit_centers_errors = np.array(
|
|
1520
|
+
[unconstrained_fit.best_errors[
|
|
1521
|
+
unconstrained_fit.best_parameters().index(f'peak{i+1}_center')]
|
|
1522
|
+
for i in range(num_peak)])
|
|
1523
|
+
unconstrained_fit_amplitudes = [
|
|
1524
|
+
unconstrained_fit.best_values[
|
|
1525
|
+
unconstrained_fit.best_parameters().index(
|
|
1526
|
+
f'peak{i+1}_amplitude')]
|
|
1527
|
+
for i in range(num_peak)]
|
|
1528
|
+
unconstrained_fit_amplitudes_errors = [
|
|
1529
|
+
unconstrained_fit.best_errors[
|
|
1530
|
+
unconstrained_fit.best_parameters().index(
|
|
1531
|
+
f'peak{i+1}_amplitude')]
|
|
1532
|
+
for i in range(num_peak)]
|
|
1533
|
+
unconstrained_fit_sigmas = [
|
|
1534
|
+
unconstrained_fit.best_values[
|
|
1535
|
+
unconstrained_fit.best_parameters().index(f'peak{i+1}_sigma')]
|
|
1536
|
+
for i in range(num_peak)]
|
|
1537
|
+
unconstrained_fit_sigmas_errors = [
|
|
1538
|
+
unconstrained_fit.best_errors[
|
|
1539
|
+
unconstrained_fit.best_parameters().index(f'peak{i+1}_sigma')]
|
|
1540
|
+
for i in range(num_peak)]
|
|
1541
|
+
if not np.asarray(unconstrained_success).all():
|
|
1542
|
+
for n in range(num_peak):
|
|
1543
|
+
unconstrained_fit_centers[n] = np.where(
|
|
1544
|
+
unconstrained_success, unconstrained_fit_centers[n],
|
|
1545
|
+
peak_locations[n])
|
|
1546
|
+
unconstrained_fit_centers_errors[n] *= unconstrained_success
|
|
1547
|
+
unconstrained_fit_amplitudes[n] *= unconstrained_success
|
|
1548
|
+
unconstrained_fit_amplitudes_errors[n] *= unconstrained_success
|
|
1549
|
+
unconstrained_fit_sigmas[n] *= unconstrained_success
|
|
1550
|
+
unconstrained_fit_sigmas_errors[n] *= unconstrained_success
|
|
1551
|
+
|
|
1552
|
+
return (
|
|
1553
|
+
{'centers': uniform_fit_centers,
|
|
1554
|
+
'centers_errors': uniform_fit_centers_errors,
|
|
1555
|
+
'amplitudes': uniform_fit_amplitudes,
|
|
1556
|
+
'amplitudes_errors': uniform_fit_amplitudes_errors,
|
|
1557
|
+
'sigmas': uniform_fit_sigmas,
|
|
1558
|
+
'sigmas_errors': uniform_fit_sigmas_errors,
|
|
1559
|
+
'best_fits': uniform_fit.best_fit,
|
|
1560
|
+
'residuals': uniform_fit.residual,
|
|
1561
|
+
'redchis': uniform_fit.redchi,
|
|
1562
|
+
'success': uniform_success},
|
|
1563
|
+
{'centers': unconstrained_fit_centers,
|
|
1564
|
+
'centers_errors': unconstrained_fit_centers_errors,
|
|
1565
|
+
'amplitudes': unconstrained_fit_amplitudes,
|
|
1566
|
+
'amplitudes_errors': unconstrained_fit_amplitudes_errors,
|
|
1567
|
+
'sigmas': unconstrained_fit_sigmas,
|
|
1568
|
+
'sigmas_errors': unconstrained_fit_sigmas_errors,
|
|
1569
|
+
'best_fits': unconstrained_fit.best_fit,
|
|
1570
|
+
'residuals': unconstrained_fit.residual,
|
|
1571
|
+
'redchis': unconstrained_fit.redchi,
|
|
1572
|
+
'success': unconstrained_success})
|