ChessAnalysisPipeline 0.0.11__py3-none-any.whl → 0.0.13__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.

Potentially problematic release.


This version of ChessAnalysisPipeline might be problematic. Click here for more details.

CHAP/edd/utils.py CHANGED
@@ -1,55 +1,84 @@
1
1
  """Utility functions for EDD workflows"""
2
2
 
3
- from scipy.constants import physical_constants
4
- hc = 1e7 * physical_constants['Planck constant in eV/Hz'][0] \
5
- * physical_constants['speed of light in vacuum'][0]
3
+ # System modules
4
+ from copy import deepcopy
6
5
 
6
+ # Third party modules
7
+ import numpy as np
7
8
 
8
- def make_material(name, sgnum, lparms, dmin=0.6):
9
- """Return a hexrd.material.Material with the given properties.
10
- Taken from
11
- [CHEDDr](https://gitlab01.classe.cornell.edu/msn-c/cheddr/-/blob/master/notebooks/CHESS_EDD.py#L99).
9
+ def get_peak_locations(ds, tth):
10
+ """Return the peak locations for a given set of lattice spacings
11
+ and 2&theta value.
12
+
13
+ :param ds: A set of lattice spacings in angstroms.
14
+ :type ds: list[float]
15
+ :param tth: Diffraction angle 2&theta.
16
+ :type tth: float
17
+ :return: The peak locations in keV.
18
+ :rtype: numpy.ndarray
19
+ """
20
+ # Third party modules
21
+ from scipy.constants import physical_constants
22
+
23
+ hc = 1e7 * physical_constants['Planck constant in eV/Hz'][0] \
24
+ * physical_constants['speed of light in vacuum'][0]
12
25
 
26
+ return hc / (2. * ds * np.sin(0.5 * np.radians(tth)))
13
27
 
14
- :param name: material name
28
+ def make_material(name, sgnum, lattice_parameters, dmin=0.6):
29
+ """Return a hexrd.material.Material with the given properties.
30
+
31
+ :param name: Material name.
15
32
  :type name: str
16
- :param sgnum: spage group number
33
+ :param sgnum: Space group of the material.
17
34
  :type sgnum: int
18
- :param lparms: lattice parameters ([a, b, c, α, β,
19
- γ], or fewer as the symmetry of the space group allows --
20
- for instance, a cubic lattice with space group number 225 can
21
- just provide [a, ])
22
- :type lparms: list[float]
23
- :param dmin: dmin of the material in Angstrom (Å), defaults to 0.6
35
+ :param lattice_parameters: The material's lattice parameters
36
+ ([a, b, c, α, β, γ], or fewer as the symmetry of
37
+ the space group allows --- for instance, a cubic lattice with
38
+ space group number 225 can just provide [a, ]).
39
+ :type lattice_parameters: list[float]
40
+ :param dmin: Materials's dmin value in angstroms (Å),
41
+ defaults to `0.6`.
24
42
  :type dmin: float, optional
25
- :return: a hexrd material
43
+ :return: A hexrd material.
26
44
  :rtype: heard.material.Material
27
45
  """
46
+ # Third party modules
28
47
  from hexrd.material import Material
29
48
  from hexrd.valunits import valWUnit
30
- import numpy as np
31
- matl = Material(name=name)
32
- matl.sgnum = sgnum
33
- if isinstance(lparms, float):
34
- lparms = [lparms]
35
- matl.latticeParameters = lparms
36
- matl.dmin = valWUnit('lp', 'length', dmin, 'angstrom')
37
- nhkls = len(matl.planeData.exclusions)
38
- matl.planeData.set_exclusions(np.zeros(nhkls, dtype=bool))
39
- return matl
49
+
50
+ material = Material()
51
+ material.name = name
52
+ material.sgnum = sgnum
53
+ if isinstance(lattice_parameters, float):
54
+ lattice_parameters = [lattice_parameters]
55
+ material.latticeParameters = lattice_parameters
56
+ material.dmin = valWUnit('lp', 'length', dmin, 'angstrom')
57
+ nhkls = len(material.planeData.exclusions)
58
+ material.planeData.set_exclusions(np.zeros(nhkls, dtype=bool))
59
+
60
+ return material
40
61
 
41
62
  def get_unique_hkls_ds(materials, tth_tol=None, tth_max=None, round_sig=8):
42
- """Return the unique HKLs and d-spacings for the given list of
43
- materials.
63
+ """Return the unique HKLs and lattice spacings for the given list
64
+ of materials.
44
65
 
45
- :param materials: list of materials to get HKLs and d-spacings for
66
+ :param materials: Materials to get HKLs and lattice spacings for.
46
67
  :type materials: list[hexrd.material.Material]
47
- :return: unique HKLs, unique d-spacings
68
+ :param tth_tol: Minimum resolvable difference in 2&theta between
69
+ two unique HKL peaks.
70
+ :type tth_tol: float, optional
71
+ :param tth_max: Detector rotation about hutch x axis.
72
+ :type tth_max: float, optional
73
+ :param round_sig: The number of significant figures in the unique
74
+ lattice spacings, defaults to `8`.
75
+ :type round_sig: int, optional
76
+ :return: Unique HKLs, unique lattice spacings.
48
77
  :rtype: tuple[np.ndarray, np.ndarray]
49
78
  """
50
- from copy import deepcopy
51
- import numpy as np
79
+ # Local modules
52
80
  from CHAP.edd.models import MaterialConfig
81
+
53
82
  _materials = deepcopy(materials)
54
83
  for i, m in enumerate(materials):
55
84
  if isinstance(m, MaterialConfig):
@@ -75,234 +104,228 @@ def get_unique_hkls_ds(materials, tth_tol=None, tth_max=None, round_sig=8):
75
104
  # Limit the list to unique lattice spacings
76
105
  hkls_unique = hkls[ds_index_unique,:].astype(int)
77
106
  ds_unique = ds[ds_index_unique]
78
- return hkls_unique, ds_unique
79
107
 
108
+ return hkls_unique, ds_unique
80
109
 
81
- def select_hkls(detector, materials, tth, y, x, interactive):
82
- """Return a plot of `detector.fit_hkls` as a matplotlib
83
- figure. Optionally modify `detector.fit_hkls` by interacting with
84
- a matplotlib figure.
85
-
86
- :param detector: the detector to set `fit_hkls` on
87
- :type detector: MCAElementConfig
88
- :param material: the material to pick HKLs for
89
- :type material: MaterialConfig
90
- :param tth: diffraction angle two-theta
91
- :type tth: float
92
- :param y: reference y data to plot
93
- :type y: np.ndarray
94
- :param x: reference x data to plot
95
- :type x: np.ndarray
96
- :param interactive: show the plot and allow user interactions with
97
- the matplotlib figure
98
- :type interactive: bool
99
- :return: plot showing the user-selected HKLs
100
- :rtype: matplotlib.figure.Figure
101
- """
102
- import numpy as np
103
- hkls, ds = get_unique_hkls_ds(materials)
104
- peak_locations = hc / (2. * ds * np.sin(0.5 * np.radians(tth)))
105
- pre_selected_peak_indices = detector.fit_hkls \
106
- if detector.fit_hkls else []
107
- from CHAP.utils.general import select_peaks
108
- selected_peaks, figure = select_peaks(
109
- y, x, peak_locations,
110
- peak_labels=[str(hkl)[1:-1] for hkl in hkls],
111
- pre_selected_peak_indices=pre_selected_peak_indices,
112
- mask=detector.mca_mask(),
113
- interactive=interactive,
114
- xlabel='MCA channel energy (keV)',
115
- ylabel='MCA intensity (counts)',
116
- title='Mask and HKLs for fitting')
117
-
118
- selected_hkl_indices = [int(np.where(peak_locations == peak)[0][0]) \
119
- for peak in selected_peaks]
120
- detector.fit_hkls = selected_hkl_indices
121
-
122
- return figure
123
-
124
-
125
- def select_tth_initial_guess(detector, material, y, x):
110
+ def select_tth_initial_guess(x, y, hkls, ds, tth_initial_guess=5.0,
111
+ interactive=False):
126
112
  """Show a matplotlib figure of a reference MCA spectrum on top of
127
113
  HKL locations. The figure includes an input field to adjust the
128
- initial tth guess and responds by updating the HKL locations based
129
- on the adjusted value of the initial tth guess.
130
-
131
- :param detector: the detector to set `tth_inital_guess` on
132
- :type detector: MCAElementConfig
133
- :param material: the material to show HKLs for
134
- :type material: MaterialConfig
135
- :param y: reference y data to plot
136
- :type y: np.ndarray
137
- :param x: reference x data to plot
114
+ initial 2&theta guess and responds by updating the HKL locations
115
+ based on the adjusted value of the initial 2&theta guess.
116
+
117
+ :param x: MCA channel energies.
138
118
  :type x: np.ndarray
139
- :return: None
119
+ :param y: MCA intensities.
120
+ :type y: np.ndarray
121
+ :param hkls: List of unique HKL indices to fit peaks for in the
122
+ calibration routine.
123
+ :type fit_hkls: Union(numpy.ndarray, list[list[int, int,int]])
124
+ :param ds: Lattice spacings in angstroms associated with the
125
+ unique HKL indices.
126
+ :type ds: Union(numpy.ndarray, list[float])
127
+ :ivar tth_initial_guess: Initial guess for 2&theta,
128
+ defaults to `5.0`.
129
+ :type tth_initial_guess: float, optional
130
+ :param interactive: Allows for user interactions, defaults to
131
+ `False`.
132
+ :type interactive: bool, optional
133
+ :return: A saveable matplotlib figure and the selected initial
134
+ guess for 2&theta.
135
+ :type: matplotlib.figure.Figure, float
140
136
  """
137
+ # Third party modules
141
138
  import matplotlib.pyplot as plt
142
139
  from matplotlib.widgets import Button, TextBox
143
- import numpy as np
144
140
 
145
- tth_initial_guess = detector.tth_initial_guess \
146
- if detector.tth_initial_guess is not None \
147
- else 5.0
148
- hkls, ds = material.unique_ds(
149
- tth_tol=detector.hkl_tth_tol, tth_max=detector.tth_max)
150
- def get_peak_locations(tth):
151
- return hc / (2. * ds * np.sin(0.5 * np.radians(tth)))
141
+ def change_fig_title(title):
142
+ if fig_title:
143
+ fig_title[0].remove()
144
+ fig_title.pop()
145
+ fig_title.append(plt.figtext(*title_pos, title, **title_props))
146
+
147
+ def change_error_text(error):
148
+ if error_texts:
149
+ error_texts[0].remove()
150
+ error_texts.pop()
151
+ error_texts.append(plt.figtext(*error_pos, error, **error_props))
152
+
153
+ def new_guess(tth):
154
+ """Callback function for the tth input."""
155
+ try:
156
+ tth_new_guess = float(tth)
157
+ except:
158
+ change_error_text(
159
+ r'Invalid 2$\theta$ 'f'cannot convert {tth} to float, '
160
+ r'enter a valid 2$\theta$')
161
+ return
162
+ for i, (loc, hkl) in enumerate(zip(
163
+ get_peak_locations(ds, tth_new_guess), hkls)):
164
+ if i in hkl_peaks:
165
+ j = hkl_peaks.index(i)
166
+ hkl_lines[j].remove()
167
+ hkl_lbls[j].remove()
168
+ if x[0] <= loc <= x[-1]:
169
+ hkl_lines[j] = ax.axvline(loc, c='k', ls='--', lw=1)
170
+ hkl_lbls[j] = ax.text(loc, 1, str(hkls[i])[1:-1],
171
+ ha='right', va='top', rotation=90,
172
+ transform=ax.get_xaxis_transform())
173
+ else:
174
+ hkl_peaks.pop(j)
175
+ hkl_lines.pop(j)
176
+ hkl_lbls.pop(j)
177
+ elif x[0] <= loc <= x[-1]:
178
+ hkl_peaks.append(i)
179
+ hkl_lines.append(ax.axvline(loc, c='k', ls='--', lw=1))
180
+ hkl_lbls.append(
181
+ ax.text(
182
+ loc, 1, str(hkl)[1:-1], ha='right', va='top',
183
+ rotation=90, transform=ax.get_xaxis_transform()))
184
+ ax.get_figure().canvas.draw()
185
+
186
+ def confirm(event):
187
+ """Callback function for the "Confirm" button."""
188
+ change_fig_title(r'Initial guess for 2$\theta$='f'{tth_input.text}')
189
+ plt.close()
190
+
191
+ fig_title = []
192
+ error_texts = []
193
+
194
+ title_pos = (0.5, 0.95)
195
+ title_props = {'fontsize': 'xx-large', 'ha': 'center', 'va': 'bottom'}
196
+ error_pos = (0.5, 0.90)
197
+ error_props = {'fontsize': 'x-large', 'ha': 'center', 'va': 'bottom'}
198
+
199
+ assert np.asarray(hkls).shape[1] == 3
200
+ assert np.asarray(ds).size == np.asarray(hkls).shape[0]
152
201
 
153
202
  fig, ax = plt.subplots(figsize=(11, 8.5))
154
203
  ax.plot(x, y)
155
204
  ax.set_xlabel('MCA channel energy (keV)')
156
205
  ax.set_ylabel('MCA intensity (counts)')
157
- ax.set_title('Adjust initial guess for $2\\theta$')
206
+ ax.set_xlim(x[0], x[-1])
207
+ peak_locations = get_peak_locations(ds, tth_initial_guess)
208
+ hkl_peaks = [i for i, loc in enumerate(peak_locations)
209
+ if x[0] <= loc <= x[-1]]
158
210
  hkl_lines = [ax.axvline(loc, c='k', ls='--', lw=1) \
159
- for loc in get_peak_locations(tth_initial_guess)]
211
+ for loc in peak_locations[hkl_peaks]]
160
212
  hkl_lbls = [ax.text(loc, 1, str(hkl)[1:-1],
161
213
  ha='right', va='top', rotation=90,
162
- transform=ax.get_xaxis_transform()) \
163
- for loc, hkl in zip(get_peak_locations(tth_initial_guess),
164
- hkls)]
214
+ transform=ax.get_xaxis_transform())
215
+ for loc, hkl in zip(peak_locations[hkl_peaks], hkls)]
165
216
 
166
- # Callback for tth input
167
- def new_guess(tth):
168
- try:
169
- tth = float(tth)
170
- except:
171
- raise ValueError(f'Cannot convert {new_tth} to float')
172
- for i, (line, loc) in enumerate(zip(hkl_lines,
173
- get_peak_locations(tth))):
174
- line.remove()
175
- hkl_lines[i] = ax.axvline(loc, c='k', ls='--', lw=1)
176
- hkl_lbls[i].remove()
177
- hkl_lbls[i] = ax.text(loc, 1, str(hkls[i])[1:-1],
178
- ha='right', va='top', rotation=90,
179
- transform=ax.get_xaxis_transform())
180
- ax.get_figure().canvas.draw()
181
- detector.tth_initial_guess = tth
217
+ if not interactive:
182
218
 
183
- # Setup tth input
184
- plt.subplots_adjust(bottom=0.25)
185
- tth_input = TextBox(plt.axes([0.125, 0.05, 0.15, 0.075]),
186
- '$2\\theta$: ',
187
- initial=tth_initial_guess)
188
- cid_update_tth = tth_input.on_submit(new_guess)
219
+ change_fig_title(r'Initial guess for 2$\theta$='f'{tth_initial_guess}')
189
220
 
190
- # Setup "Confirm" button
191
- def confirm_selection(event):
192
- plt.close()
193
- confirm_b = Button(plt.axes([0.75, 0.05, 0.15, 0.075]), 'Confirm')
194
- cid_confirm = confirm_b.on_clicked(confirm_selection)
221
+ else:
222
+
223
+ change_fig_title(r'Adjust initial guess for 2$\theta$')
224
+ fig.subplots_adjust(bottom=0.2)
225
+
226
+ # Setup tth input
227
+ tth_input = TextBox(plt.axes([0.125, 0.05, 0.15, 0.075]),
228
+ '$2\\theta$: ',
229
+ initial=tth_initial_guess)
230
+ cid_update_tth = tth_input.on_submit(new_guess)
231
+
232
+ # Setup "Confirm" button
233
+ confirm_btn = Button(plt.axes([0.75, 0.05, 0.15, 0.075]), 'Confirm')
234
+ confirm_cid = confirm_btn.on_clicked(confirm)
195
235
 
196
- # Show figure for user interaction
197
- plt.show()
236
+ # Show figure for user interaction
237
+ plt.show()
198
238
 
199
- # Disconnect all widget callbacks when figure is closed
200
- tth_input.disconnect(cid_update_tth)
201
- confirm_b.disconnect(cid_confirm)
239
+ # Disconnect all widget callbacks when figure is closed
240
+ tth_input.disconnect(cid_update_tth)
241
+ confirm_btn.disconnect(confirm_cid)
202
242
 
243
+ # ...and remove the buttons before returning the figure
244
+ tth_input.ax.remove()
245
+ confirm_btn.ax.remove()
203
246
 
247
+ fig_title[0].set_in_layout(True)
248
+ fig.tight_layout(rect=(0, 0, 1, 0.95))
204
249
 
205
- def select_material_params(x, y, tth, materials=[]):
206
- # name=Material.DFLT_NAME,
207
- # sgnum=Material.DFLT_SGNUM,
208
- # lparms=Material.DFLT_LPARMS,
209
- # dmin=0.6):
210
- """Interactively select lattice parameters and space group for a
211
- list of materials. A matplotlib figure will be shown with a plot
250
+ if not interactive:
251
+ tth_new_guess = tth_initial_guess
252
+ else:
253
+ try:
254
+ tth_new_guess = float(tth_input.text)
255
+ except:
256
+ fig, tth_new_guess = select_tth_initial_guess(
257
+ x, y, hkls, ds, tth_initial_guess, interactive)
258
+
259
+ return fig, tth_new_guess
260
+
261
+ def select_material_params(x, y, tth, materials=[], interactive=False):
262
+ """Interactively select the lattice parameters and space group for
263
+ a list of materials. A matplotlib figure will be shown with a plot
212
264
  of the reference data (`x` and `y`). The figure will contain
213
265
  widgets to add / remove materials and update selections for space
214
266
  group number and lattice parameters for each one. The HKLs for the
215
267
  materials defined by the widgets' values will be shown over the
216
268
  reference data and updated when the widgets' values are
217
- updated. Return a list of the selected materials when the figure
218
- is closed.
269
+ updated.
270
+
271
+ :param x: MCA channel energies.
272
+ :type x: np.ndarray
273
+ :param y: MCA intensities.
274
+ :type y: np.ndarray
275
+ :param tth: The (calibrated) 2&theta angle.
276
+ :type tth: float
277
+ :param materials: Materials to get HKLs and lattice spacings for.
278
+ :type materials: list[hexrd.material.Material]
279
+ :param interactive: Allows for user interactions, defaults to
280
+ `False`.
281
+ :type interactive: bool, optional
282
+ :return: A saveable matplotlib figure and the selected materials
283
+ for the strain analyses.
284
+ :rtype: matplotlib.figure.Figure,
285
+ list[CHAP.edd.models.MaterialConfig]
219
286
  """
220
- from copy import deepcopy
287
+ # Third party modules
221
288
  import matplotlib.pyplot as plt
222
289
  from matplotlib.widgets import Button, TextBox
223
- import numpy as np
224
- from CHAP.edd.models import MaterialConfig
225
290
 
226
- _materials = deepcopy(materials)
227
- for i, m in enumerate(_materials):
228
- if isinstance(m, MaterialConfig):
229
- _materials[i] = m._material
291
+ # Local modules
292
+ from CHAP.edd.models import MaterialConfig
230
293
 
231
- # Set up plot of reference data
232
- fig, ax = plt.subplots(figsize=(11, 8.5))
294
+ def change_error_text(error):
295
+ if error_texts:
296
+ error_texts[0].remove()
297
+ error_texts.pop()
298
+ error_texts.append(plt.figtext(*error_pos, error, **error_props))
233
299
 
234
300
  def draw_plot():
301
+ """Redraw plot of reference data and HKL locations based on
302
+ the `_materials` list on the Matplotlib axes `ax`.
303
+ """
235
304
  ax.clear()
236
305
  ax.set_title('Reference Data')
237
306
  ax.set_xlabel('MCA channel energy (keV)')
238
307
  ax.set_ylabel('MCA intensity (counts)')
308
+ ax.set_xlim(x[0], x[-1])
239
309
  ax.plot(x, y)
240
310
  for i, material in enumerate(_materials):
241
311
  hkls, ds = get_unique_hkls_ds([material])
242
- E0s = hc / (2. * ds * np.sin(0.5 * np.radians(tth)))
312
+ E0s = get_peak_locations(ds, tth)
243
313
  for hkl, E0 in zip(hkls, E0s):
244
- ax.axvline(E0, c=f'C{i}', ls='--', lw=1)
245
- ax.text(E0, 1, str(hkl)[1:-1], c=f'C{i}',
246
- ha='right', va='top', rotation=90,
247
- transform=ax.get_xaxis_transform())
314
+ if x[0] <= E0 <= x[-1]:
315
+ ax.axvline(E0, c=f'C{i}', ls='--', lw=1)
316
+ ax.text(E0, 1, str(hkl)[1:-1], c=f'C{i}',
317
+ ha='right', va='top', rotation=90,
318
+ transform=ax.get_xaxis_transform())
248
319
  ax.get_figure().canvas.draw()
249
320
 
250
- # Confirm & close button
251
- widget_callbacks = []
252
- plt.subplots_adjust(bottom=0.1)
253
- def confirm_selection(event):
254
- plt.close()
255
- confirm_button = Button(plt.axes([0.75, 0.015, 0.1, 0.05]), 'Confirm')
256
- cid_confirm = confirm_button.on_clicked(confirm_selection)
257
- widget_callbacks.append((confirm_button, cid_confirm))
258
-
259
- # Widgets to edit materials
260
- widgets = []
261
- def update_materials(*args, **kwargs):
262
- """Validate input material properties and redraw the plot"""
263
- materials_ok = True
264
- for i, (material,
265
- (name_input, sgnum_input, \
266
- a_input, b_input, c_input, \
267
- alpha_input, beta_input, gamma_input)) \
268
- in enumerate(zip(materials, widgets)):
269
- name = name_input.text
270
- print(f'looking at parms for material {name}')
271
- try:
272
- sgnum = int(sgnum_input.text)
273
- sgnum_input.color = '.95'
274
- _materials[i].sgnum = sgnum
275
- except:
276
- sgnum_input.color = 'red'
277
- materials_ok = False
278
- lparms = []
279
- lparms_ok = True
280
- for lparm_input in (a_input, b_input, c_input,
281
- alpha_input, beta_input, gamma_input):
282
- try:
283
- lparm = float(lparm_input.text)
284
- lparm_input.color = '.95'
285
- except:
286
- lparm_input.color = 'red'
287
- materials_ok = False
288
- lparms_ok = False
289
- else:
290
- lparms.append(lparm)
291
- if lparms_ok:
292
- _materials[i].latticeParameters = lparms
293
-
294
-
295
- if materials_ok:
296
- confirm_button.active = True
297
- else:
298
- confirm_button.active = False
299
- # update items in the `materials` list, then:
300
- draw_plot()
301
-
302
321
  def add_material(*args, material=None, new=True):
303
- """Add new row of material-property-editing widgets to the
304
- figure and update the plot with new HKLs
322
+ """Callback function for the "Add material" button to add
323
+ a new row of material-property-editing widgets to the figure
324
+ and update the plot with new HKLs.
305
325
  """
326
+ if error_texts:
327
+ error_texts[0].remove()
328
+ error_texts.pop()
306
329
  if material is None:
307
330
  material = make_material('new_material', 225, 3.0)
308
331
  _materials.append(material)
@@ -310,7 +333,7 @@ def select_material_params(x, y, tth, materials=[]):
310
333
  material = material._material
311
334
  bottom = len(_materials) * 0.075
312
335
  plt.subplots_adjust(bottom=bottom + 0.125)
313
- name_input = TextBox(plt.axes([0.125, bottom, 0.06, 0.05]),
336
+ name_input = TextBox(plt.axes([0.1, bottom, 0.09, 0.05]),
314
337
  'Material: ',
315
338
  initial=material.name)
316
339
  sgnum_input = TextBox(plt.axes([0.3, bottom, 0.06, 0.05]),
@@ -337,28 +360,516 @@ def select_material_params(x, y, tth, materials=[]):
337
360
  widgets.append(
338
361
  (name_input, sgnum_input, a_input, b_input, c_input,
339
362
  alpha_input, beta_input, gamma_input))
340
- for widget in widgets[-1]:
341
- widget_callbacks.append(
342
- (widget, widget.on_submit(update_materials)))
363
+ widget_callbacks.append(
364
+ [(widget, widget.on_submit(update_materials)) \
365
+ for widget in widgets[-1]])
366
+ draw_plot()
367
+
368
+ def update_materials(*args, **kwargs):
369
+ """Callback function for the material-property-editing widgets
370
+ button to validate input material properties from widgets,
371
+ update the `_materials` list, and redraw the plot.
372
+ """
373
+ def set_vals(material_i):
374
+ """Set all widget values from the `_materials` list for a
375
+ particular material.
376
+ """
377
+ material = _materials[material_i]
378
+ # Temporarily disconnect widget callbacks
379
+ callbacks = widget_callbacks[material_i+2]
380
+ for widget, callback in callbacks:
381
+ widget.disconnect(callback)
382
+ # Set widget values
383
+ name_input, sgnum_input, \
384
+ a_input, b_input, c_input, \
385
+ alpha_input, beta_input, gamma_input = widgets[material_i]
386
+ name_input.set_val(material.name)
387
+ sgnum_input.set_val(material.sgnum)
388
+ a_input.set_val(material.latticeParameters[0].value)
389
+ b_input.set_val(material.latticeParameters[1].value)
390
+ c_input.set_val(material.latticeParameters[2].value)
391
+ alpha_input.set_val(material.latticeParameters[3].value)
392
+ beta_input.set_val(material.latticeParameters[4].value)
393
+ gamma_input.set_val(material.latticeParameters[5].value)
394
+ # Reconnect widget callbacks
395
+ for i, (w, cb) in enumerate(widget_callbacks[material_i+2]):
396
+ widget_callbacks[material_i+2][i] = (
397
+ w, w.on_submit(update_materials))
398
+
399
+ # Update the _materials list
400
+ for i, (material,
401
+ (name_input, sgnum_input,
402
+ a_input, b_input, c_input,
403
+ alpha_input, beta_input, gamma_input)) \
404
+ in enumerate(zip(_materials, widgets)):
405
+ # Skip if no parameters were changes on this material
406
+ old_material_params = (
407
+ material.name, material.sgnum,
408
+ [material.latticeParameters[i].value for i in range(6)]
409
+ )
410
+ new_material_params = (
411
+ name_input.text, int(sgnum_input.text),
412
+ [float(a_input.text), float(b_input.text), float(c_input.text),
413
+ float(alpha_input.text), float(beta_input.text),
414
+ float(gamma_input.text)]
415
+ )
416
+ if old_material_params == new_material_params:
417
+ continue
418
+ try:
419
+ new_material = make_material(*new_material_params)
420
+ except:
421
+ change_error_text(f'Bad input for {material.name}')
422
+ else:
423
+ _materials[i] = new_material
424
+ finally:
425
+ set_vals(i)
426
+
427
+ # Redraw reference data plot
343
428
  draw_plot()
344
429
 
345
- # Button to add materials
346
- add_material_button = Button(
347
- plt.axes([0.125, 0.015, 0.1, 0.05]), 'Add material')
348
- cid_add_material = add_material_button.on_clicked(add_material)
349
- widget_callbacks.append((add_material_button, cid_add_material))
350
-
351
- # Draw data & show plot
352
- for material in _materials:
353
- add_material(material=material)
354
- plt.show()
355
-
356
- # Teardown after figure is closed
357
- for widget, callback in widget_callbacks:
358
- widget.disconnect(callback)
359
-
360
- new_materials = [MaterialConfig(
361
- material_name=m.name, sgnum=m.sgnum,
362
- lattice_parameters=[m.latticeParameters[i].value for i in range(6)]) \
363
- for m in _materials]
364
- return new_materials
430
+ def confirm(event):
431
+ """Callback function for the "Confirm" button."""
432
+ if error_texts:
433
+ error_texts[0].remove()
434
+ error_texts.pop()
435
+ plt.close()
436
+
437
+ widgets = []
438
+ widget_callbacks = []
439
+ error_texts = []
440
+
441
+ error_pos = (0.5, 0.95)
442
+ error_props = {'fontsize': 'x-large', 'ha': 'center', 'va': 'bottom'}
443
+
444
+ _materials = deepcopy(materials)
445
+ for i, m in enumerate(_materials):
446
+ if isinstance(m, MaterialConfig):
447
+ _materials[i] = m._material
448
+
449
+ # Set up plot of reference data
450
+ fig, ax = plt.subplots(figsize=(11, 8.5))
451
+
452
+ if interactive:
453
+
454
+ plt.subplots_adjust(bottom=0.1)
455
+
456
+ # Setup "Add material" button
457
+ add_material_btn = Button(
458
+ plt.axes([0.125, 0.015, 0.1, 0.05]), 'Add material')
459
+ add_material_cid = add_material_btn.on_clicked(add_material)
460
+ widget_callbacks.append([(add_material_btn, add_material_cid)])
461
+
462
+ # Setup "Confirm" button
463
+ confirm_btn = Button(plt.axes([0.75, 0.015, 0.1, 0.05]), 'Confirm')
464
+ confirm_cid = confirm_btn.on_clicked(confirm)
465
+ widget_callbacks.append([(confirm_btn, confirm_cid)])
466
+
467
+ # Setup material-property-editing buttons for each material
468
+ for material in _materials:
469
+ add_material(material=material)
470
+
471
+ # Show figure for user interaction
472
+ plt.show()
473
+
474
+ # Disconnect all widget callbacks when figure is closed
475
+ # and remove the buttons before returning the figure
476
+ for group in widget_callbacks:
477
+ for widget, callback in group:
478
+ widget.disconnect(callback)
479
+ widget.ax.remove()
480
+
481
+ fig.tight_layout()
482
+
483
+ new_materials = [
484
+ MaterialConfig(
485
+ material_name=m.name, sgnum=m.sgnum,
486
+ lattice_parameters=[
487
+ m.latticeParameters[i].value for i in range(6)])
488
+ for m in _materials]
489
+
490
+ return fig, new_materials
491
+
492
+ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
493
+ preselected_hkl_indices=[], detector_name=None, ref_map=None,
494
+ flux_energy_range=None, calibration_bin_ranges=None,
495
+ interactive=False):
496
+ """Return a matplotlib figure to indicate data ranges and HKLs to
497
+ include for fitting in EDD Ceria calibration and/or strain
498
+ analysis.
499
+
500
+ :param x: MCA channel energies.
501
+ :type x: np.ndarray
502
+ :param y: MCA intensities.
503
+ :type y: np.ndarray
504
+ :param hkls: Avaliable Unique HKL values to fit peaks for in the
505
+ calibration routine.
506
+ :type hkls: list[list[int]]
507
+ :param ds: Lattice spacings associated with the unique HKL indices
508
+ in angstroms.
509
+ :type ds: list[float]
510
+ :param tth: The (calibrated) 2&theta angle.
511
+ :type tth: float
512
+ :param preselected_bin_ranges: Preselected MCA channel index ranges
513
+ whose data should be included after applying a mask,
514
+ defaults to `[]`
515
+ :type preselected_bin_ranges: list[list[int]], optional
516
+ :param preselected_hkl_indices: Preselected unique HKL indices to
517
+ fit peaks for in the calibration routine, defaults to `[]`.
518
+ :type preselected_hkl_indices: list[int], optional
519
+ :param detector_name: Name of the MCA detector element.
520
+ :type detector_name: str, optional
521
+ :param ref_map: Reference map of MCA intensities to show underneath
522
+ the interactive plot.
523
+ :type ref_map: np.ndarray, optional
524
+ :param flux_energy_range: Energy range in eV in the flux file
525
+ containing station beam energy in eV versus flux
526
+ :type flux_energy_range: tuple(float, float), optional
527
+ :param calibration_bin_ranges: MCA channel index ranges included
528
+ in the detector calibration.
529
+ :type calibration_bin_ranges: list[[int, int]], optional
530
+ :param interactive: Allows for user interactions, defaults to
531
+ `False`.
532
+ :type interactive: bool, optional
533
+ :return: A saveable matplotlib figure, the list of selected data
534
+ index ranges to include, and the list of HKL indices to
535
+ include
536
+ :rtype: matplotlib.figure.Figure, list[list[int]], list[int]
537
+ """
538
+ # Third party modules
539
+ import matplotlib.lines as mlines
540
+ from matplotlib.patches import Patch
541
+ import matplotlib.pyplot as plt
542
+ from matplotlib.widgets import Button, SpanSelector
543
+
544
+ # Local modules
545
+ from CHAP.utils.general import (
546
+ get_consecutive_int_range,
547
+ index_nearest_down,
548
+ index_nearest_upp,
549
+ )
550
+
551
+ def change_fig_title(title):
552
+ if fig_title:
553
+ fig_title[0].remove()
554
+ fig_title.pop()
555
+ fig_title.append(plt.figtext(*title_pos, title, **title_props))
556
+
557
+ def change_error_text(error):
558
+ if error_texts:
559
+ error_texts[0].remove()
560
+ error_texts.pop()
561
+ error_texts.append(plt.figtext(*error_pos, error, **error_props))
562
+
563
+ def hkl_locations_in_any_span(hkl_index):
564
+ """Return the index of the span where the location of a specific
565
+ HKL resides. Return(-1 if outside any span."""
566
+ if hkl_index < 0 or hkl_index>= len(hkl_locations):
567
+ return -1
568
+ for i, span in enumerate(spans):
569
+ if (span.extents[0] <= hkl_locations[hkl_index] and
570
+ span.extents[1] >= hkl_locations[hkl_index]):
571
+ return i
572
+ return -1
573
+
574
+ def on_span_select(xmin, xmax):
575
+ """Callback function for the SpanSelector widget."""
576
+ removed_hkls = False
577
+ if not init_flag[0]:
578
+ for hkl_index in deepcopy(selected_hkl_indices):
579
+ if hkl_locations_in_any_span(hkl_index) < 0:
580
+ hkl_vlines[hkl_index].set(**excluded_hkl_props)
581
+ selected_hkl_indices.remove(hkl_index)
582
+ removed_hkls = True
583
+ combined_spans = False
584
+ combined_spans_test = True
585
+ while combined_spans_test:
586
+ combined_spans_test = False
587
+ for i, span1 in enumerate(spans):
588
+ for span2 in reversed(spans[i+1:]):
589
+ if (span1.extents[1] >= span2.extents[0]
590
+ and span1.extents[0] <= span2.extents[1]):
591
+ span1.extents = (
592
+ min(span1.extents[0], span2.extents[0]),
593
+ max(span1.extents[1], span2.extents[1]))
594
+ span2.set_visible(False)
595
+ spans.remove(span2)
596
+ combined_spans = True
597
+ combined_spans_test = True
598
+ break
599
+ if combined_spans_test:
600
+ break
601
+ if flux_energy_range is not None:
602
+ for span in spans:
603
+ min_ = max(span.extents[0], min_x)
604
+ max_ = min(span.extents[1], max_x)
605
+ span.extents = (min_, max_)
606
+ added_hkls = False
607
+ for hkl_index in range(len(hkl_locations)):
608
+ if (hkl_index not in selected_hkl_indices
609
+ and hkl_locations_in_any_span(hkl_index) >= 0):
610
+ hkl_vlines[hkl_index].set(**included_hkl_props)
611
+ selected_hkl_indices.append(hkl_index)
612
+ added_hkls = True
613
+ if combined_spans:
614
+ if added_hkls or removed_hkls:
615
+ change_error_text(
616
+ 'Combined overlapping spans and selected only HKL(s) '
617
+ 'inside the selected energy mask')
618
+ else:
619
+ change_error_text(
620
+ 'Combined overlapping spans in the selected energy mask')
621
+ elif added_hkls and removed_hkls:
622
+ change_error_text(
623
+ 'Adjusted the selected HKL(s) to match the selected '
624
+ 'energy mask')
625
+ elif added_hkls:
626
+ change_error_text(
627
+ 'Added HKL(s) to match the selected energy mask')
628
+ elif removed_hkls:
629
+ change_error_text(
630
+ 'Removed HKL(s) outside the selected energy mask')
631
+ plt.draw()
632
+
633
+ def add_span(event, xrange_init=None):
634
+ """Callback function for the "Add span" button."""
635
+ spans.append(
636
+ SpanSelector(
637
+ ax, on_span_select, 'horizontal', props=included_data_props,
638
+ useblit=True, interactive=interactive, drag_from_anywhere=True,
639
+ ignore_event_outside=True, grab_range=5))
640
+ if xrange_init is None:
641
+ xmin_init = min_x
642
+ xmax_init = 0.5*(min_x + hkl_locations[0])
643
+ else:
644
+ xmin_init = max(min_x, xrange_init[0])
645
+ xmax_init = min(max_x, xrange_init[1])
646
+ spans[-1]._selection_completed = True
647
+ spans[-1].extents = (xmin_init, xmax_init)
648
+ spans[-1].onselect(xmin_init, xmax_init)
649
+
650
+ def pick_hkl(event):
651
+ """The "onpick" callback function."""
652
+ try:
653
+ hkl_index = hkl_vlines.index(event.artist)
654
+ except:
655
+ pass
656
+ else:
657
+ hkl_vline = event.artist
658
+ if hkl_index in deepcopy(selected_hkl_indices):
659
+ hkl_vline.set(**excluded_hkl_props)
660
+ selected_hkl_indices.remove(hkl_index)
661
+ span = spans[hkl_locations_in_any_span(hkl_index)]
662
+ span_next_hkl_index = hkl_locations_in_any_span(hkl_index+1)
663
+ span_prev_hkl_index = hkl_locations_in_any_span(hkl_index-1)
664
+ if span_next_hkl_index < 0 and span_prev_hkl_index < 0:
665
+ span.set_visible(False)
666
+ spans.remove(span)
667
+ elif span_next_hkl_index < 0:
668
+ span.extents = (
669
+ span.extents[0],
670
+ 0.5*(hkl_locations[hkl_index-1]
671
+ + hkl_locations[hkl_index]))
672
+ elif span_prev_hkl_index < 0:
673
+ span.extents = (
674
+ 0.5*(hkl_locations[hkl_index]
675
+ + hkl_locations[hkl_index+1]),
676
+ span.extents[1])
677
+ else:
678
+ xrange_init = [
679
+ 0.5*(hkl_locations[hkl_index]
680
+ + hkl_locations[hkl_index+1]),
681
+ span.extents[1]]
682
+ span.extents = (
683
+ span.extents[0],
684
+ 0.5*(hkl_locations[hkl_index-1]
685
+ + hkl_locations[hkl_index]))
686
+ add_span(None, xrange_init=xrange_init)
687
+ change_error_text(
688
+ f'Adjusted the selected energy mask to reflect the '
689
+ 'removed HKL')
690
+ else:
691
+ change_error_text(
692
+ f'Selected HKL is outside any current span, '
693
+ 'extend or add spans before adding this value')
694
+ plt.draw()
695
+
696
+ def reset(event):
697
+ """Callback function for the "Confirm" button."""
698
+ for hkl_index in deepcopy(selected_hkl_indices):
699
+ hkl_vlines[hkl_index].set(**excluded_hkl_props)
700
+ selected_hkl_indices.remove(hkl_index)
701
+ for span in reversed(spans):
702
+ span.set_visible(False)
703
+ spans.remove(span)
704
+ plt.draw()
705
+
706
+ def confirm(event):
707
+ """Callback function for the "Confirm" button."""
708
+ if not len(spans) or len(selected_hkl_indices) < 2:
709
+ change_error_text('Select at least one span and two HKLs')
710
+ plt.draw()
711
+ else:
712
+ if error_texts:
713
+ error_texts[0].remove()
714
+ error_texts.pop()
715
+ if detector_name is None:
716
+ change_fig_title('Selected data and HKLs used in fitting')
717
+ else:
718
+ change_fig_title(
719
+ f'Selected data and HKLs used in fitting {detector_name}')
720
+ plt.close()
721
+
722
+ selected_hkl_indices = preselected_hkl_indices
723
+ spans = []
724
+ hkl_vlines = []
725
+ fig_title = []
726
+ error_texts = []
727
+
728
+ hkl_locations = [loc for loc in get_peak_locations(ds, tth)
729
+ if x[0] <= loc <= x[-1]]
730
+ hkl_labels = [str(hkl)[1:-1] for hkl, loc in zip(hkls, hkl_locations)]
731
+
732
+ title_pos = (0.5, 0.95)
733
+ title_props = {'fontsize': 'xx-large', 'ha': 'center', 'va': 'bottom'}
734
+ error_pos = (0.5, 0.90)
735
+ error_props = {'fontsize': 'x-large', 'ha': 'center', 'va': 'bottom'}
736
+ excluded_hkl_props = {
737
+ 'color': 'black', 'linestyle': '--','linewidth': 1,
738
+ 'marker': 10, 'markersize': 5, 'fillstyle': 'none'}
739
+ included_hkl_props = {
740
+ 'color': 'green', 'linestyle': '-', 'linewidth': 2,
741
+ 'marker': 10, 'markersize': 10, 'fillstyle': 'full'}
742
+ excluded_data_props = {
743
+ 'facecolor': 'white', 'edgecolor': 'gray', 'linestyle': ':'}
744
+ included_data_props = {
745
+ 'alpha': 0.5, 'facecolor': 'tab:blue', 'edgecolor': 'blue'}
746
+
747
+ if flux_energy_range is None:
748
+ min_x = x.min()
749
+ max_x = x.max()
750
+ else:
751
+ min_x = x[index_nearest_upp(x, max(x.min(), flux_energy_range[0]))]
752
+ max_x = x[index_nearest_down(x, min(x.max(), flux_energy_range[1]))]
753
+
754
+ if ref_map is None:
755
+ fig, ax = plt.subplots(figsize=(11, 8.5))
756
+ ax.set(xlabel='Energy (keV)', ylabel='Intensity (counts)')
757
+ else:
758
+ fig, (ax, ax_map) = plt.subplots(
759
+ 2, sharex=True, figsize=(11, 8.5), height_ratios=[2, 1])
760
+ ax.set(ylabel='Intensity (counts)')
761
+ ax_map.pcolormesh(x, np.arange(ref_map.shape[0]), ref_map)
762
+ ax_map.set_yticks([])
763
+ ax_map.set_xlabel('Energy (keV)')
764
+ ax_map.set_xlim(x[0], x[-1])
765
+ handles = ax.plot(x, y, color='k', label='Reference Data')
766
+ if calibration_bin_ranges is not None:
767
+ ylow = ax.get_ylim()[0]
768
+ for low, upp in calibration_bin_ranges:
769
+ ax.plot([x[low], x[upp]], [ylow, ylow], color='r', linewidth=2)
770
+ handles.append(mlines.Line2D(
771
+ [], [], label='Energies included in calibration', color='r',
772
+ linewidth=2))
773
+ handles.append(mlines.Line2D(
774
+ [], [], label='Excluded / unselected HKL', **excluded_hkl_props))
775
+ handles.append(mlines.Line2D(
776
+ [], [], label='Included / selected HKL', **included_hkl_props))
777
+ handles.append(Patch(
778
+ label='Excluded / unselected data', **excluded_data_props))
779
+ handles.append(Patch(
780
+ label='Included / selected data', **included_data_props))
781
+ ax.legend(handles=handles)
782
+ ax.set_xlim(x[0], x[-1])
783
+
784
+ if selected_hkl_indices and not preselected_bin_ranges:
785
+ index_ranges = get_consecutive_int_range(selected_hkl_indices)
786
+ for index_range in index_ranges:
787
+ i = index_range[0]
788
+ if i:
789
+ min_ = 0.5*(hkl_locations[i-1] + hkl_locations[i])
790
+ else:
791
+ min_ = 0.5*(min_x + hkl_locations[i])
792
+ j = index_range[1]
793
+ if j < len(hkl_locations)-1:
794
+ max_ = 0.5*(hkl_locations[j] + hkl_locations[j+1])
795
+ else:
796
+ max_ = 0.5*(hkl_locations[j] + max_x)
797
+ preselected_bin_ranges.append(
798
+ [index_nearest_upp(x, min_), index_nearest_down(x, max_)])
799
+
800
+ for i, (loc, lbl) in enumerate(zip(hkl_locations, hkl_labels)):
801
+ nearest_index = np.searchsorted(x, loc)
802
+ if i in selected_hkl_indices:
803
+ hkl_vline = ax.axvline(loc, **included_hkl_props)
804
+ else:
805
+ hkl_vline = ax.axvline(loc, **excluded_hkl_props)
806
+ ax.text(loc, 1, lbl, ha='right', va='top', rotation=90,
807
+ transform=ax.get_xaxis_transform())
808
+ hkl_vlines.append(hkl_vline)
809
+
810
+ init_flag = [True]
811
+ for bin_range in preselected_bin_ranges:
812
+ add_span(None, xrange_init=x[bin_range])
813
+ init_flag = [False]
814
+
815
+ if not interactive:
816
+
817
+ if detector_name is None:
818
+ change_fig_title('Selected data and HKLs used in fitting')
819
+ else:
820
+ change_fig_title(
821
+ f'Selected data and HKLs used in fitting {detector_name}')
822
+
823
+ else:
824
+
825
+ if detector_name is None:
826
+ change_fig_title('Select data and HKLs to use in fitting')
827
+ else:
828
+ change_fig_title(
829
+ f'Select data and HKLs to use in fitting {detector_name}')
830
+ fig.subplots_adjust(bottom=0.2)
831
+
832
+ # Setup "Add span" button
833
+ add_span_btn = Button(plt.axes([0.125, 0.05, 0.15, 0.075]), 'Add span')
834
+ add_span_cid = add_span_btn.on_clicked(add_span)
835
+
836
+ for vline in hkl_vlines:
837
+ vline.set_picker(5)
838
+ pick_hkl_cid = fig.canvas.mpl_connect('pick_event', pick_hkl)
839
+
840
+ # Setup "Reset" button
841
+ reset_btn = Button(plt.axes([0.4375, 0.05, 0.15, 0.075]), 'Reset')
842
+ reset_cid = reset_btn.on_clicked(reset)
843
+
844
+ # Setup "Confirm" button
845
+ confirm_btn = Button(plt.axes([0.75, 0.05, 0.15, 0.075]), 'Confirm')
846
+ confirm_cid = confirm_btn.on_clicked(confirm)
847
+
848
+ # Show figure for user interaction
849
+ plt.show()
850
+
851
+ # Disconnect all widget callbacks when figure is closed
852
+ add_span_btn.disconnect(add_span_cid)
853
+ fig.canvas.mpl_disconnect(pick_hkl_cid)
854
+ reset_btn.disconnect(reset_cid)
855
+ confirm_btn.disconnect(confirm_cid)
856
+
857
+ # ...and remove the buttons before returning the figure
858
+ add_span_btn.ax.remove()
859
+ confirm_btn.ax.remove()
860
+ reset_btn.ax.remove()
861
+ plt.subplots_adjust(bottom=0.0)
862
+
863
+ selected_bin_ranges = [np.searchsorted(x, span.extents).tolist()
864
+ for span in spans]
865
+ if not selected_bin_ranges:
866
+ selected_bin_ranges = None
867
+ if selected_hkl_indices:
868
+ selected_hkl_indices = sorted(selected_hkl_indices)
869
+ else:
870
+ selected_hkl_indices = None
871
+
872
+ fig_title[0].set_in_layout(True)
873
+ fig.tight_layout(rect=(0, 0, 1, 0.95))
874
+
875
+ return fig, selected_bin_ranges, selected_hkl_indices