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.
Files changed (70) hide show
  1. CHAP/TaskManager.py +216 -0
  2. CHAP/__init__.py +27 -0
  3. CHAP/common/__init__.py +57 -0
  4. CHAP/common/models/__init__.py +8 -0
  5. CHAP/common/models/common.py +124 -0
  6. CHAP/common/models/integration.py +659 -0
  7. CHAP/common/models/map.py +1291 -0
  8. CHAP/common/processor.py +2869 -0
  9. CHAP/common/reader.py +658 -0
  10. CHAP/common/utils.py +110 -0
  11. CHAP/common/writer.py +730 -0
  12. CHAP/edd/__init__.py +23 -0
  13. CHAP/edd/models.py +876 -0
  14. CHAP/edd/processor.py +3069 -0
  15. CHAP/edd/reader.py +1023 -0
  16. CHAP/edd/select_material_params_gui.py +348 -0
  17. CHAP/edd/utils.py +1572 -0
  18. CHAP/edd/writer.py +26 -0
  19. CHAP/foxden/__init__.py +19 -0
  20. CHAP/foxden/models.py +71 -0
  21. CHAP/foxden/processor.py +124 -0
  22. CHAP/foxden/reader.py +224 -0
  23. CHAP/foxden/utils.py +80 -0
  24. CHAP/foxden/writer.py +168 -0
  25. CHAP/giwaxs/__init__.py +11 -0
  26. CHAP/giwaxs/models.py +491 -0
  27. CHAP/giwaxs/processor.py +776 -0
  28. CHAP/giwaxs/reader.py +8 -0
  29. CHAP/giwaxs/writer.py +8 -0
  30. CHAP/inference/__init__.py +7 -0
  31. CHAP/inference/processor.py +69 -0
  32. CHAP/inference/reader.py +8 -0
  33. CHAP/inference/writer.py +8 -0
  34. CHAP/models.py +227 -0
  35. CHAP/pipeline.py +479 -0
  36. CHAP/processor.py +125 -0
  37. CHAP/reader.py +124 -0
  38. CHAP/runner.py +277 -0
  39. CHAP/saxswaxs/__init__.py +7 -0
  40. CHAP/saxswaxs/processor.py +8 -0
  41. CHAP/saxswaxs/reader.py +8 -0
  42. CHAP/saxswaxs/writer.py +8 -0
  43. CHAP/server.py +125 -0
  44. CHAP/sin2psi/__init__.py +7 -0
  45. CHAP/sin2psi/processor.py +8 -0
  46. CHAP/sin2psi/reader.py +8 -0
  47. CHAP/sin2psi/writer.py +8 -0
  48. CHAP/tomo/__init__.py +15 -0
  49. CHAP/tomo/models.py +210 -0
  50. CHAP/tomo/processor.py +3862 -0
  51. CHAP/tomo/reader.py +9 -0
  52. CHAP/tomo/writer.py +59 -0
  53. CHAP/utils/__init__.py +6 -0
  54. CHAP/utils/converters.py +188 -0
  55. CHAP/utils/fit.py +2947 -0
  56. CHAP/utils/general.py +2655 -0
  57. CHAP/utils/material.py +274 -0
  58. CHAP/utils/models.py +595 -0
  59. CHAP/utils/parfile.py +224 -0
  60. CHAP/writer.py +122 -0
  61. MLaaS/__init__.py +0 -0
  62. MLaaS/ktrain.py +205 -0
  63. MLaaS/mnist_img.py +83 -0
  64. MLaaS/tfaas_client.py +371 -0
  65. chessanalysispipeline-0.0.17.dev3.dist-info/LICENSE +60 -0
  66. chessanalysispipeline-0.0.17.dev3.dist-info/METADATA +29 -0
  67. chessanalysispipeline-0.0.17.dev3.dist-info/RECORD +70 -0
  68. chessanalysispipeline-0.0.17.dev3.dist-info/WHEEL +5 -0
  69. chessanalysispipeline-0.0.17.dev3.dist-info/entry_points.txt +2 -0
  70. 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})