ChessAnalysisPipeline 0.0.13__py3-none-any.whl → 0.0.15__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
@@ -25,6 +25,7 @@ def get_peak_locations(ds, tth):
25
25
 
26
26
  return hc / (2. * ds * np.sin(0.5 * np.radians(tth)))
27
27
 
28
+
28
29
  def make_material(name, sgnum, lattice_parameters, dmin=0.6):
29
30
  """Return a hexrd.material.Material with the given properties.
30
31
 
@@ -59,6 +60,7 @@ def make_material(name, sgnum, lattice_parameters, dmin=0.6):
59
60
 
60
61
  return material
61
62
 
63
+
62
64
  def get_unique_hkls_ds(materials, tth_tol=None, tth_max=None, round_sig=8):
63
65
  """Return the unique HKLs and lattice spacings for the given list
64
66
  of materials.
@@ -107,8 +109,9 @@ def get_unique_hkls_ds(materials, tth_tol=None, tth_max=None, round_sig=8):
107
109
 
108
110
  return hkls_unique, ds_unique
109
111
 
112
+
110
113
  def select_tth_initial_guess(x, y, hkls, ds, tth_initial_guess=5.0,
111
- interactive=False):
114
+ interactive=False, filename=None):
112
115
  """Show a matplotlib figure of a reference MCA spectrum on top of
113
116
  HKL locations. The figure includes an input field to adjust the
114
117
  initial 2&theta guess and responds by updating the HKL locations
@@ -127,13 +130,19 @@ def select_tth_initial_guess(x, y, hkls, ds, tth_initial_guess=5.0,
127
130
  :ivar tth_initial_guess: Initial guess for 2&theta,
128
131
  defaults to `5.0`.
129
132
  :type tth_initial_guess: float, optional
130
- :param interactive: Allows for user interactions, defaults to
131
- `False`.
133
+ :param interactive: Show the plot and allow user interactions with
134
+ the matplotlib figure, defaults to `True`.
132
135
  :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
136
+ :param filename: Save a .png of the plot to filename, defaults to
137
+ `None`, in which case the plot is not saved.
138
+ :type filename: str, optional
139
+ :type interactive: bool, optional
140
+ :return: The selected initial guess for 2&theta.
141
+ :type: float
136
142
  """
143
+ if not interactive and filename is None:
144
+ return tth_initial_guess
145
+
137
146
  # Third party modules
138
147
  import matplotlib.pyplot as plt
139
148
  from matplotlib.widgets import Button, TextBox
@@ -191,14 +200,15 @@ def select_tth_initial_guess(x, y, hkls, ds, tth_initial_guess=5.0,
191
200
  fig_title = []
192
201
  error_texts = []
193
202
 
203
+ assert np.asarray(hkls).shape[1] == 3
204
+ assert np.asarray(ds).size == np.asarray(hkls).shape[0]
205
+
206
+ # Setup the Matplotlib figure
194
207
  title_pos = (0.5, 0.95)
195
208
  title_props = {'fontsize': 'xx-large', 'ha': 'center', 'va': 'bottom'}
196
209
  error_pos = (0.5, 0.90)
197
210
  error_props = {'fontsize': 'x-large', 'ha': 'center', 'va': 'bottom'}
198
211
 
199
- assert np.asarray(hkls).shape[1] == 3
200
- assert np.asarray(ds).size == np.asarray(hkls).shape[0]
201
-
202
212
  fig, ax = plt.subplots(figsize=(11, 8.5))
203
213
  ax.plot(x, y)
204
214
  ax.set_xlabel('MCA channel energy (keV)')
@@ -244,8 +254,12 @@ def select_tth_initial_guess(x, y, hkls, ds, tth_initial_guess=5.0,
244
254
  tth_input.ax.remove()
245
255
  confirm_btn.ax.remove()
246
256
 
247
- fig_title[0].set_in_layout(True)
248
- fig.tight_layout(rect=(0, 0, 1, 0.95))
257
+ # Save the figures if requested and close
258
+ if filename is not None:
259
+ fig_title[0].set_in_layout(True)
260
+ fig.tight_layout(rect=(0, 0, 1, 0.95))
261
+ fig.savefig(filename)
262
+ plt.close()
249
263
 
250
264
  if not interactive:
251
265
  tth_new_guess = tth_initial_guess
@@ -253,12 +267,14 @@ def select_tth_initial_guess(x, y, hkls, ds, tth_initial_guess=5.0,
253
267
  try:
254
268
  tth_new_guess = float(tth_input.text)
255
269
  except:
256
- fig, tth_new_guess = select_tth_initial_guess(
257
- x, y, hkls, ds, tth_initial_guess, interactive)
270
+ tth_new_guess = select_tth_initial_guess(
271
+ x, y, hkls, ds, tth_initial_guess, interactive, filename)
272
+
273
+ return tth_new_guess
258
274
 
259
- return fig, tth_new_guess
260
275
 
261
- def select_material_params(x, y, tth, materials=[], interactive=False):
276
+ def select_material_params_old(x, y, tth, materials=[], label='Reference Data',
277
+ interactive=False, filename=None):
262
278
  """Interactively select the lattice parameters and space group for
263
279
  a list of materials. A matplotlib figure will be shown with a plot
264
280
  of the reference data (`x` and `y`). The figure will contain
@@ -276,17 +292,22 @@ def select_material_params(x, y, tth, materials=[], interactive=False):
276
292
  :type tth: float
277
293
  :param materials: Materials to get HKLs and lattice spacings for.
278
294
  :type materials: list[hexrd.material.Material]
279
- :param interactive: Allows for user interactions, defaults to
280
- `False`.
295
+ :param label: Legend label for the 1D plot of reference MCA data
296
+ from the parameters `x`, `y`, defaults to `"Reference Data"`
297
+ :type label: str, optional
298
+ :param interactive: Show the plot and allow user interactions with
299
+ the matplotlib figure, defaults to `False`.
281
300
  :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]
301
+ :param filename: Save a .png of the plot to filename, defaults to
302
+ `None`, in which case the plot is not saved.
303
+ :type filename: str, optional
304
+ :return: The selected materials for the strain analyses.
305
+ :rtype: list[CHAP.edd.models.MaterialConfig]
286
306
  """
287
307
  # Third party modules
288
- import matplotlib.pyplot as plt
289
- from matplotlib.widgets import Button, TextBox
308
+ if interactive or filename is not None:
309
+ import matplotlib.pyplot as plt
310
+ from matplotlib.widgets import Button, TextBox
290
311
 
291
312
  # Local modules
292
313
  from CHAP.edd.models import MaterialConfig
@@ -306,7 +327,7 @@ def select_material_params(x, y, tth, materials=[], interactive=False):
306
327
  ax.set_xlabel('MCA channel energy (keV)')
307
328
  ax.set_ylabel('MCA intensity (counts)')
308
329
  ax.set_xlim(x[0], x[-1])
309
- ax.plot(x, y)
330
+ ax.plot(x, y, label=label)
310
331
  for i, material in enumerate(_materials):
311
332
  hkls, ds = get_unique_hkls_ds([material])
312
333
  E0s = get_peak_locations(ds, tth)
@@ -316,6 +337,7 @@ def select_material_params(x, y, tth, materials=[], interactive=False):
316
337
  ax.text(E0, 1, str(hkl)[1:-1], c=f'C{i}',
317
338
  ha='right', va='top', rotation=90,
318
339
  transform=ax.get_xaxis_transform())
340
+ ax.legend()
319
341
  ax.get_figure().canvas.draw()
320
342
 
321
343
  def add_material(*args, material=None, new=True):
@@ -438,63 +460,323 @@ def select_material_params(x, y, tth, materials=[], interactive=False):
438
460
  widget_callbacks = []
439
461
  error_texts = []
440
462
 
441
- error_pos = (0.5, 0.95)
442
- error_props = {'fontsize': 'x-large', 'ha': 'center', 'va': 'bottom'}
443
-
444
463
  _materials = deepcopy(materials)
445
464
  for i, m in enumerate(_materials):
446
465
  if isinstance(m, MaterialConfig):
447
466
  _materials[i] = m._material
448
467
 
449
- # Set up plot of reference data
468
+ # Create the Matplotlib figure
469
+ if interactive or filename is not None:
470
+ fig, ax = plt.subplots(figsize=(11, 8.5))
471
+
472
+ if not interactive:
473
+
474
+ draw_plot()
475
+
476
+ else:
477
+
478
+ error_pos = (0.5, 0.95)
479
+ error_props = {
480
+ 'fontsize': 'x-large', 'ha': 'center', 'va': 'bottom'}
481
+
482
+ plt.subplots_adjust(bottom=0.1)
483
+
484
+ # Setup "Add material" button
485
+ add_material_btn = Button(
486
+ plt.axes([0.125, 0.015, 0.1, 0.05]), 'Add material')
487
+ add_material_cid = add_material_btn.on_clicked(add_material)
488
+ widget_callbacks.append([(add_material_btn, add_material_cid)])
489
+
490
+ # Setup "Confirm" button
491
+ confirm_btn = Button(plt.axes([0.75, 0.015, 0.1, 0.05]), 'Confirm')
492
+ confirm_cid = confirm_btn.on_clicked(confirm)
493
+ widget_callbacks.append([(confirm_btn, confirm_cid)])
494
+
495
+ # Setup material-property-editing buttons for each material
496
+ for material in _materials:
497
+ add_material(material=material)
498
+
499
+ # Show figure for user interaction
500
+ plt.show()
501
+
502
+ # Disconnect all widget callbacks when figure is closed
503
+ # and remove the buttons before returning the figure
504
+ for group in widget_callbacks:
505
+ for widget, callback in group:
506
+ widget.disconnect(callback)
507
+ widget.ax.remove()
508
+
509
+ # Save the figures if requested and close
510
+ fig.tight_layout()
511
+ if filename is not None:
512
+ fig.savefig(filename)
513
+ plt.close()
514
+
515
+ new_materials = [
516
+ MaterialConfig(
517
+ material_name=m.name, sgnum=m.sgnum,
518
+ lattice_parameters=[
519
+ m.latticeParameters[i].value for i in range(6)])
520
+ for m in _materials]
521
+
522
+ return new_materials
523
+
524
+
525
+ def select_material_params(
526
+ x, y, tth, preselected_materials=[], label='Reference Data',
527
+ interactive=False, filename=None):
528
+ """Interactively select the lattice parameters and space group for
529
+ a list of materials. A matplotlib figure will be shown with a plot
530
+ of the reference data (`x` and `y`). The figure will contain
531
+ widgets to add / remove materials and update selections for space
532
+ group number and lattice parameters for each one. The HKLs for the
533
+ materials defined by the widgets' values will be shown over the
534
+ reference data and updated when the widgets' values are
535
+ updated.
536
+
537
+ :param x: MCA channel energies.
538
+ :type x: np.ndarray
539
+ :param y: MCA intensities.
540
+ :type y: np.ndarray
541
+ :param tth: The (calibrated) 2&theta angle.
542
+ :type tth: float
543
+ :param materials: Materials to get HKLs and lattice spacings for,
544
+ default to `[]`.
545
+ :type materials: list[hexrd.material.Material], optional
546
+ :param label: Legend label for the 1D plot of reference MCA data
547
+ from the parameters `x`, `y`, defaults to `"Reference Data"`
548
+ :type label: str, optional
549
+ :param interactive: Show the plot and allow user interactions with
550
+ the matplotlib figure, defaults to `False`.
551
+ :type interactive: bool, optional
552
+ :param filename: Save a .png of the plot to filename, defaults to
553
+ `None`, in which case the plot is not saved.
554
+ :type filename: str, optional
555
+ :return: The selected materials for the strain analyses.
556
+ :rtype: list[CHAP.edd.models.MaterialConfig]
557
+ """
558
+ # Third party modules
559
+ if interactive or filename is not None:
560
+ from hexrd.material import Material
561
+ import matplotlib.pyplot as plt
562
+ from matplotlib.widgets import Button, TextBox, RadioButtons
563
+
564
+ # Local modules
565
+ from CHAP.edd.models import MaterialConfig
566
+ from CHAP.utils.general import round_to_n
567
+
568
+ def add_material(new_material, add_buttons):
569
+ if isinstance(new_material, Material):
570
+ m = new_material
571
+ else:
572
+ if not isinstance(new_material, MaterialConfig):
573
+ new_material = MaterialConfig(**new_material)
574
+ m = new_material._material
575
+ materials.append(m)
576
+ lat_params = [round_to_n(m.latticeParameters[i].value, 6)
577
+ for i in range(6)]
578
+ bottom = 0.05*len(materials)
579
+ if interactive:
580
+ bottom += 0.075
581
+ mat_texts.append(
582
+ plt.figtext(
583
+ 0.15, bottom,
584
+ f'- {m.name}: sgnum = {m.sgnum}, lat params = {lat_params}',
585
+ fontsize='large', ha='left', va='center'))
586
+ def add(event):
587
+ """Callback function for the "Add" button."""
588
+ added_material.append(True)
589
+ plt.close()
590
+
591
+ def remove(event):
592
+ """Callback function for the "Remove" button."""
593
+ for mat_text in mat_texts:
594
+ mat_text.remove()
595
+ mat_texts.clear()
596
+ for button in buttons:
597
+ button[0].disconnect(button[1])
598
+ button[0].ax.remove()
599
+ buttons.clear()
600
+ if len(materials) == 1:
601
+ removed_material.clear()
602
+ removed_material.append(materials[0].name)
603
+ plt.close()
604
+ else:
605
+ def remove_material(label):
606
+ removed_material.clear()
607
+ removed_material.append(label)
608
+ radio_btn.disconnect(radio_cid)
609
+ radio_btn.ax.remove()
610
+ plt.close()
611
+
612
+ mat_texts.append(
613
+ plt.figtext(
614
+ 0.1, 0.1 + 0.05*len(materials),
615
+ 'Select a material to remove:',
616
+ fontsize='x-large', ha='left', va='center'))
617
+ radio_btn = RadioButtons(
618
+ plt.axes([0.1, 0.05, 0.3, 0.05*len(materials)]),
619
+ labels = list(reversed([m.name for m in materials])),
620
+ activecolor='k')
621
+ removed_material.append(radio_btn.value_selected)
622
+ radio_cid = radio_btn.on_clicked(remove_material)
623
+ plt.draw()
624
+
625
+ def accept(event):
626
+ """Callback function for the "Accept" button."""
627
+ plt.close()
628
+
629
+ materials = []
630
+ added_material = []
631
+ removed_material = []
632
+ mat_texts = []
633
+ buttons = []
634
+
635
+ # Create figure
450
636
  fig, ax = plt.subplots(figsize=(11, 8.5))
637
+ ax.set_title(label, fontsize='x-large')
638
+ ax.set_xlabel('MCA channel energy (keV)', fontsize='large')
639
+ ax.set_ylabel('MCA intensity (counts)', fontsize='large')
640
+ ax.set_xlim(x[0], x[-1])
641
+ ax.plot(x, y)
451
642
 
452
- if interactive:
643
+ # Add materials
644
+ for m in reversed(preselected_materials):
645
+ add_material(m, interactive)
646
+
647
+ # Add materials to figure
648
+ for i, material in enumerate(materials):
649
+ hkls, ds = get_unique_hkls_ds([material])
650
+ E0s = get_peak_locations(ds, tth)
651
+ for hkl, E0 in zip(hkls, E0s):
652
+ if x[0] <= E0 <= x[-1]:
653
+ ax.axvline(E0, c=f'C{i}', ls='--', lw=1)
654
+ ax.text(E0, 1, str(hkl)[1:-1], c=f'C{i}',
655
+ ha='right', va='top', rotation=90,
656
+ transform=ax.get_xaxis_transform())
453
657
 
454
- plt.subplots_adjust(bottom=0.1)
658
+ if not interactive:
455
659
 
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)])
660
+ if materials:
661
+ mat_texts.append(
662
+ plt.figtext(
663
+ 0.1, 0.05 + 0.05*len(materials),
664
+ 'Currently selected materials:',
665
+ fontsize='x-large', ha='left', va='center'))
666
+ plt.subplots_adjust(bottom=0.125 + 0.05*len(materials))
461
667
 
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)])
668
+ else:
466
669
 
467
- # Setup material-property-editing buttons for each material
468
- for material in _materials:
469
- add_material(material=material)
670
+ if materials:
671
+ mat_texts.append(
672
+ plt.figtext(
673
+ 0.1, 0.125 + 0.05*len(materials),
674
+ 'Currently selected materials:',
675
+ fontsize='x-large', ha='left', va='center'))
676
+ else:
677
+ mat_texts.append(
678
+ plt.figtext(
679
+ 0.1, 0.125, 'Add at least one material',
680
+ fontsize='x-large', ha='left', va='center'))
681
+ plt.subplots_adjust(bottom=0.2 + 0.05*len(materials))
682
+
683
+ # Setup "Add" button
684
+ add_btn = Button(plt.axes([0.1, 0.025, 0.15, 0.05]), 'Add material')
685
+ add_cid = add_btn.on_clicked(add)
686
+ buttons.append((add_btn, add_cid))
687
+
688
+ # Setup "Remove" button
689
+ if materials:
690
+ remove_btn = Button(
691
+ plt.axes([0.425, 0.025, 0.15, 0.05]), f'Remove material')
692
+ remove_cid = remove_btn.on_clicked(remove)
693
+ buttons.append((remove_btn, remove_cid))
694
+
695
+ # Setup "Accept" button
696
+ accept_btn = Button(
697
+ plt.axes([0.75, 0.025, 0.15, 0.05]), 'Accept materials')
698
+ accept_cid = accept_btn.on_clicked(accept)
699
+ buttons.append((accept_btn, accept_cid))
470
700
 
471
- # Show figure for user interaction
472
701
  plt.show()
473
702
 
474
703
  # Disconnect all widget callbacks when figure is closed
475
704
  # 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 = [
705
+ for button in buttons:
706
+ button[0].disconnect(button[1])
707
+ button[0].ax.remove()
708
+ buttons.clear()
709
+
710
+ if filename is not None:
711
+ for mat_text in mat_texts:
712
+ pos = mat_text.get_position()
713
+ if interactive:
714
+ mat_text.set_position((pos[0], pos[1]-0.075))
715
+ else:
716
+ mat_text.set_position(pos)
717
+ if mat_text.get_text() == 'Currently selected materials:':
718
+ mat_text.set_text('Selected materials:')
719
+ mat_text.set_in_layout(True)
720
+ fig.tight_layout(rect=(0, 0.05 + 0.05*len(materials), 1, 1))
721
+ fig.savefig(filename)
722
+ plt.close()
723
+
724
+ if added_material:
725
+ # Local modules
726
+ from CHAP.utils.general import (
727
+ input_int,
728
+ input_num_list,
729
+ )
730
+
731
+ error = True
732
+ while error:
733
+ try:
734
+ print('\nEnter the name of the material to be added:')
735
+ name = input()
736
+ sgnum = input_int(
737
+ 'Enter the space group for this material',
738
+ raise_error=True, log=False)
739
+ lat_params = input_num_list(
740
+ 'Enter the lattice properties for this material',
741
+ raise_error=True, log=False)
742
+ print()
743
+ new_material = MaterialConfig(
744
+ material_name=name, sgnum=sgnum,
745
+ lattice_parameters=lat_params)
746
+ error = False
747
+ except (
748
+ ValueError, TypeError, SyntaxError, MemoryError,
749
+ RecursionError, IndexError) as e:
750
+ print(f'{e}: try again')
751
+ except:
752
+ raise
753
+ materials.append(new_material)
754
+ return select_material_params(
755
+ x, y, tth, preselected_materials= materials, label=label,
756
+ interactive=interactive, filename=filename)
757
+ if removed_material:
758
+ return select_material_params(
759
+ x, y, tth,
760
+ preselected_materials=[
761
+ m for m in materials if m.name not in removed_material],
762
+ label=label, interactive=interactive, filename=filename)
763
+ if not materials:
764
+ return select_material_params(
765
+ x, y, tth, label=label, interactive=interactive, filename=filename)
766
+ return [
484
767
  MaterialConfig(
485
768
  material_name=m.name, sgnum=m.sgnum,
486
769
  lattice_parameters=[
487
770
  m.latticeParameters[i].value for i in range(6)])
488
- for m in _materials]
771
+ for m in materials]
489
772
 
490
- return fig, new_materials
491
773
 
492
774
  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):
775
+ preselected_hkl_indices=[], num_hkl_min=1, detector_name=None,
776
+ ref_map=None, flux_energy_range=None, calibration_bin_ranges=None,
777
+ label='Reference Data', interactive=False, filename=None):
496
778
  """Return a matplotlib figure to indicate data ranges and HKLs to
497
- include for fitting in EDD Ceria calibration and/or strain
779
+ include for fitting in EDD energy/tth calibration and/or strain
498
780
  analysis.
499
781
 
500
782
  :param x: MCA channel energies.
@@ -516,6 +798,9 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
516
798
  :param preselected_hkl_indices: Preselected unique HKL indices to
517
799
  fit peaks for in the calibration routine, defaults to `[]`.
518
800
  :type preselected_hkl_indices: list[int], optional
801
+ :param num_hkl_min: Minimum number of HKLs to select,
802
+ defaults to `1`.
803
+ :type num_hkl_min: int, optional
519
804
  :param detector_name: Name of the MCA detector element.
520
805
  :type detector_name: str, optional
521
806
  :param ref_map: Reference map of MCA intensities to show underneath
@@ -527,25 +812,32 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
527
812
  :param calibration_bin_ranges: MCA channel index ranges included
528
813
  in the detector calibration.
529
814
  :type calibration_bin_ranges: list[[int, int]], optional
530
- :param interactive: Allows for user interactions, defaults to
531
- `False`.
815
+ :param label: Legend label for the 1D plot of reference MCA data
816
+ from the parameters `x`, `y`, defaults to `"Reference Data"`
817
+ :type label: str, optional
818
+ :param interactive: Show the plot and allow user interactions with
819
+ the matplotlib figure, defaults to `True`.
532
820
  :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]
821
+ :param filename: Save a .png of the plot to filename, defaults to
822
+ `None`, in which case the plot is not saved.
823
+ :type filename: str, optional
824
+ :return: The list of selected data index ranges to include, and the
825
+ list of HKL indices to include
826
+ :rtype: list[list[int]], list[int]
537
827
  """
538
828
  # Third party modules
539
- import matplotlib.lines as mlines
540
- from matplotlib.patches import Patch
829
+ if interactive or filename is not None:
830
+ import matplotlib.lines as mlines
831
+ from matplotlib.patches import Patch
832
+ from matplotlib.widgets import Button
541
833
  import matplotlib.pyplot as plt
542
- from matplotlib.widgets import Button, SpanSelector
834
+ from matplotlib.widgets import SpanSelector
543
835
 
544
836
  # Local modules
545
837
  from CHAP.utils.general import (
546
838
  get_consecutive_int_range,
547
839
  index_nearest_down,
548
- index_nearest_upp,
840
+ index_nearest_up,
549
841
  )
550
842
 
551
843
  def change_fig_title(title):
@@ -560,10 +852,20 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
560
852
  error_texts.pop()
561
853
  error_texts.append(plt.figtext(*error_pos, error, **error_props))
562
854
 
855
+ def get_mask():
856
+ """Return a boolean array that acts as the mask corresponding
857
+ to the currently-selected index ranges"""
858
+ mask = np.full(x.shape[0], False)
859
+ for span in spans:
860
+ _min, _max = span.extents
861
+ mask = np.logical_or(
862
+ mask, np.logical_and(x >= _min, x <= _max))
863
+ return mask
864
+
563
865
  def hkl_locations_in_any_span(hkl_index):
564
866
  """Return the index of the span where the location of a specific
565
867
  HKL resides. Return(-1 if outside any span."""
566
- if hkl_index < 0 or hkl_index>= len(hkl_locations):
868
+ if hkl_index < 0 or hkl_index >= len(hkl_locations):
567
869
  return -1
568
870
  for i, span in enumerate(spans):
569
871
  if (span.extents[0] <= hkl_locations[hkl_index] and
@@ -571,15 +873,21 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
571
873
  return i
572
874
  return -1
573
875
 
876
+ def position_cax():
877
+ """Reposition the colorbar axes according to the axes of the
878
+ reference map"""
879
+ ((left, bottom), (right, top)) = ax_map.get_position().get_points()
880
+ cax.set_position([right + 0.01, bottom, 0.01, top - bottom])
881
+
574
882
  def on_span_select(xmin, xmax):
575
883
  """Callback function for the SpanSelector widget."""
576
884
  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:
885
+ for hkl_index in deepcopy(selected_hkl_indices):
886
+ if hkl_locations_in_any_span(hkl_index) < 0:
887
+ if interactive or filename is not None:
580
888
  hkl_vlines[hkl_index].set(**excluded_hkl_props)
581
- selected_hkl_indices.remove(hkl_index)
582
- removed_hkls = True
889
+ selected_hkl_indices.remove(hkl_index)
890
+ removed_hkls = True
583
891
  combined_spans = False
584
892
  combined_spans_test = True
585
893
  while combined_spans_test:
@@ -607,27 +915,37 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
607
915
  for hkl_index in range(len(hkl_locations)):
608
916
  if (hkl_index not in selected_hkl_indices
609
917
  and hkl_locations_in_any_span(hkl_index) >= 0):
610
- hkl_vlines[hkl_index].set(**included_hkl_props)
918
+ if interactive or filename is not None:
919
+ hkl_vlines[hkl_index].set(**included_hkl_props)
611
920
  selected_hkl_indices.append(hkl_index)
612
921
  added_hkls = True
613
- if combined_spans:
614
- if added_hkls or removed_hkls:
922
+ if interactive or filename is not None:
923
+ if combined_spans:
924
+ if added_hkls or removed_hkls:
925
+ change_error_text(
926
+ 'Combined overlapping spans and selected only HKL(s) '
927
+ 'inside the selected energy mask')
928
+ else:
929
+ change_error_text('Combined overlapping spans in the '
930
+ 'selected energy mask')
931
+ elif added_hkls and removed_hkls:
615
932
  change_error_text(
616
- 'Combined overlapping spans and selected only HKL(s) '
617
- 'inside the selected energy mask')
618
- else:
933
+ 'Adjusted the selected HKL(s) to match the selected '
934
+ 'energy mask')
935
+ elif added_hkls:
619
936
  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')
937
+ 'Added HKL(s) to match the selected energy mask')
938
+ elif removed_hkls:
939
+ change_error_text(
940
+ 'Removed HKL(s) outside the selected energy mask')
941
+ # If using ref_map, update the colorbar range to min / max of
942
+ # the selected data only
943
+ if ref_map is not None:
944
+ selected_data = ref_map[:,get_mask()]
945
+ ref_map_mappable = ax_map.pcolormesh(
946
+ x, np.arange(ref_map.shape[0]), ref_map,
947
+ vmin=selected_data.min(), vmax=selected_data.max())
948
+ fig.colorbar(ref_map_mappable, cax=cax)
631
949
  plt.draw()
632
950
 
633
951
  def add_span(event, xrange_init=None):
@@ -659,17 +977,19 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
659
977
  hkl_vline.set(**excluded_hkl_props)
660
978
  selected_hkl_indices.remove(hkl_index)
661
979
  span = spans[hkl_locations_in_any_span(hkl_index)]
662
- span_next_hkl_index = hkl_locations_in_any_span(hkl_index+1)
663
980
  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:
981
+ span_curr_hkl_index = hkl_locations_in_any_span(hkl_index)
982
+ span_next_hkl_index = hkl_locations_in_any_span(hkl_index+1)
983
+ if (span_curr_hkl_index != span_prev_hkl_index
984
+ and span_curr_hkl_index != span_next_hkl_index):
665
985
  span.set_visible(False)
666
986
  spans.remove(span)
667
- elif span_next_hkl_index < 0:
987
+ elif span_curr_hkl_index != span_next_hkl_index:
668
988
  span.extents = (
669
989
  span.extents[0],
670
990
  0.5*(hkl_locations[hkl_index-1]
671
991
  + hkl_locations[hkl_index]))
672
- elif span_prev_hkl_index < 0:
992
+ elif span_curr_hkl_index != span_prev_hkl_index:
673
993
  span.extents = (
674
994
  0.5*(hkl_locations[hkl_index]
675
995
  + hkl_locations[hkl_index+1]),
@@ -688,13 +1008,54 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
688
1008
  f'Adjusted the selected energy mask to reflect the '
689
1009
  'removed HKL')
690
1010
  else:
1011
+ hkl_vline.set(**included_hkl_props)
1012
+ prev_hkl = hkl_index-1 in selected_hkl_indices
1013
+ next_hkl = hkl_index+1 in selected_hkl_indices
1014
+ if prev_hkl and next_hkl:
1015
+ span_prev = spans[hkl_locations_in_any_span(hkl_index-1)]
1016
+ span_next = spans[hkl_locations_in_any_span(hkl_index+1)]
1017
+ span_prev.extents = (
1018
+ span_prev.extents[0], span_next.extents[1])
1019
+ span_next.set_visible(False)
1020
+ elif prev_hkl:
1021
+ span_prev = spans[hkl_locations_in_any_span(hkl_index-1)]
1022
+ if hkl_index < len(hkl_locations)-1:
1023
+ max_ = 0.5*(
1024
+ hkl_locations[hkl_index]
1025
+ + hkl_locations[hkl_index+1])
1026
+ else:
1027
+ max_ = 0.5*(hkl_locations[hkl_index] + max_x)
1028
+ span_prev.extents = (span_prev.extents[0], max_)
1029
+ elif next_hkl:
1030
+ span_next = spans[hkl_locations_in_any_span(hkl_index+1)]
1031
+ if hkl_index > 0:
1032
+ min_ = 0.5*(
1033
+ hkl_locations[hkl_index-1]
1034
+ + hkl_locations[hkl_index])
1035
+ else:
1036
+ min_ = 0.5*(min_x + hkl_locations[hkl_index])
1037
+ span_next.extents = (min_, span_next.extents[1])
1038
+ else:
1039
+ if hkl_index > 0:
1040
+ min_ = 0.5*(
1041
+ hkl_locations[hkl_index-1]
1042
+ + hkl_locations[hkl_index])
1043
+ else:
1044
+ min_ = 0.5*(min_x + hkl_locations[hkl_index])
1045
+ if hkl_index < len(hkl_locations)-1:
1046
+ max_ = 0.5*(
1047
+ hkl_locations[hkl_index]
1048
+ + hkl_locations[hkl_index+1])
1049
+ else:
1050
+ max_ = 0.5*(hkl_locations[hkl_index] + max_x)
1051
+ add_span(None, xrange_init=(min_, max_))
691
1052
  change_error_text(
692
- f'Selected HKL is outside any current span, '
693
- 'extend or add spans before adding this value')
1053
+ f'Adjusted the selected energy mask to reflect the '
1054
+ 'added HKL')
694
1055
  plt.draw()
695
1056
 
696
1057
  def reset(event):
697
- """Callback function for the "Confirm" button."""
1058
+ """Callback function for the "Reset" button."""
698
1059
  for hkl_index in deepcopy(selected_hkl_indices):
699
1060
  hkl_vlines[hkl_index].set(**excluded_hkl_props)
700
1061
  selected_hkl_indices.remove(hkl_index)
@@ -705,8 +1066,9 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
705
1066
 
706
1067
  def confirm(event):
707
1068
  """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')
1069
+ if not len(spans) or len(selected_hkl_indices) < num_hkl_min:
1070
+ change_error_text(
1071
+ f'Select at least one span and {num_hkl_min} HKLs')
710
1072
  plt.draw()
711
1073
  else:
712
1074
  if error_texts:
@@ -725,62 +1087,12 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
725
1087
  fig_title = []
726
1088
  error_texts = []
727
1089
 
1090
+ if ref_map is not None and ref_map.ndim == 1:
1091
+ ref_map = None
1092
+
1093
+ # Make preselected_bin_ranges consistent with selected_hkl_indices
728
1094
  hkl_locations = [loc for loc in get_peak_locations(ds, tth)
729
1095
  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
1096
  if selected_hkl_indices and not preselected_bin_ranges:
785
1097
  index_ranges = get_consecutive_int_range(selected_hkl_indices)
786
1098
  for index_range in index_ranges:
@@ -795,30 +1107,109 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
795
1107
  else:
796
1108
  max_ = 0.5*(hkl_locations[j] + max_x)
797
1109
  preselected_bin_ranges.append(
798
- [index_nearest_upp(x, min_), index_nearest_down(x, max_)])
1110
+ [index_nearest_up(x, min_), index_nearest_down(x, max_)])
1111
+
1112
+ if flux_energy_range is None:
1113
+ min_x = x.min()
1114
+ max_x = x.max()
1115
+ else:
1116
+ min_x = x[index_nearest_up(x, max(x.min(), flux_energy_range[0]))]
1117
+ max_x = x[index_nearest_down(x, min(x.max(), flux_energy_range[1]))]
1118
+
1119
+ # Setup the Matplotlib figure
1120
+ if not interactive and filename is None:
1121
+
1122
+ # It is too convenient to not use the Matplotlib SpanSelector
1123
+ # so define a (fig, ax) tuple, despite not creating a figure
1124
+ included_data_props = {}
1125
+ fig, ax = plt.subplots()
799
1126
 
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)
1127
+ else:
1128
+
1129
+ title_pos = (0.5, 0.95)
1130
+ title_props = {'fontsize': 'xx-large', 'ha': 'center', 'va': 'bottom'}
1131
+ error_pos = (0.5, 0.90)
1132
+ error_props = {'fontsize': 'x-large', 'ha': 'center', 'va': 'bottom'}
1133
+ excluded_hkl_props = {
1134
+ 'color': 'black', 'linestyle': '--','linewidth': 1,
1135
+ 'marker': 10, 'markersize': 5, 'fillstyle': 'none'}
1136
+ included_hkl_props = {
1137
+ 'color': 'green', 'linestyle': '-', 'linewidth': 2,
1138
+ 'marker': 10, 'markersize': 10, 'fillstyle': 'full'}
1139
+ included_data_props = {
1140
+ 'alpha': 0.5, 'facecolor': 'tab:blue', 'edgecolor': 'blue'}
1141
+ excluded_data_props = {
1142
+ 'facecolor': 'white', 'edgecolor': 'gray', 'linestyle': ':'}
1143
+
1144
+ if ref_map is None:
1145
+ fig, ax = plt.subplots(figsize=(11, 8.5))
1146
+ ax.set(xlabel='Energy (keV)', ylabel='Intensity (counts)')
804
1147
  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)
1148
+ if ref_map.ndim > 2:
1149
+ ref_map = np.reshape(
1150
+ ref_map, (np.prod(ref_map.shape[:-1]), ref_map.shape[-1]))
1151
+ # If needed, abbreviate ref_map to <= 50 spectra to keep
1152
+ # response time of mouse interactions quick.
1153
+ max_ref_spectra = 50
1154
+ if ref_map.shape[0] > max_ref_spectra:
1155
+ choose_i = np.sort(
1156
+ np.random.choice(
1157
+ ref_map.shape[0], max_ref_spectra, replace=False))
1158
+ ref_map = ref_map[choose_i]
1159
+ fig, (ax, ax_map) = plt.subplots(
1160
+ 2, sharex=True, figsize=(11, 8.5), height_ratios=[2, 1])
1161
+ ax.set(ylabel='Intensity (counts)')
1162
+ ref_map_mappable = ax_map.pcolormesh(
1163
+ x, np.arange(ref_map.shape[0]), ref_map)
1164
+ ax_map.set_yticks([])
1165
+ ax_map.set_xlabel('Energy (keV)')
1166
+ ax_map.set_xlim(x[0], x[-1])
1167
+ ((left, bottom), (right, top)) = ax_map.get_position().get_points()
1168
+ cax = plt.axes([right + 0.01, bottom, 0.01, top - bottom])
1169
+ fig.colorbar(ref_map_mappable, cax=cax)
1170
+ handles = ax.plot(x, y, color='k', label=label)
1171
+ if calibration_bin_ranges is not None:
1172
+ ylow = ax.get_ylim()[0]
1173
+ for low, upp in calibration_bin_ranges:
1174
+ ax.plot([x[low], x[upp]], [ylow, ylow], color='r', linewidth=2)
1175
+ handles.append(mlines.Line2D(
1176
+ [], [], label='Energies included in calibration', color='r',
1177
+ linewidth=2))
1178
+ handles.append(mlines.Line2D(
1179
+ [], [], label='Excluded / unselected HKL', **excluded_hkl_props))
1180
+ handles.append(mlines.Line2D(
1181
+ [], [], label='Included / selected HKL', **included_hkl_props))
1182
+ handles.append(Patch(
1183
+ label='Excluded / unselected data', **excluded_data_props))
1184
+ handles.append(Patch(
1185
+ label='Included / selected data', **included_data_props))
1186
+ ax.legend(handles=handles)
1187
+ ax.set_xlim(x[0], x[-1])
809
1188
 
810
- init_flag = [True]
1189
+ # Add HKL lines
1190
+ hkl_labels = [str(hkl)[1:-1] for hkl, loc in zip(hkls, hkl_locations)]
1191
+ for i, (loc, lbl) in enumerate(zip(hkl_locations, hkl_labels)):
1192
+ nearest_index = np.searchsorted(x, loc)
1193
+ if i in selected_hkl_indices:
1194
+ hkl_vline = ax.axvline(loc, **included_hkl_props)
1195
+ else:
1196
+ hkl_vline = ax.axvline(loc, **excluded_hkl_props)
1197
+ ax.text(loc, 1, lbl, ha='right', va='top', rotation=90,
1198
+ transform=ax.get_xaxis_transform())
1199
+ hkl_vlines.append(hkl_vline)
1200
+
1201
+ # Add initial spans
811
1202
  for bin_range in preselected_bin_ranges:
812
1203
  add_span(None, xrange_init=x[bin_range])
813
- init_flag = [False]
814
1204
 
815
1205
  if not interactive:
816
1206
 
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}')
1207
+ if filename is not None:
1208
+ if detector_name is None:
1209
+ change_fig_title('Selected data and HKLs used in fitting')
1210
+ else:
1211
+ change_fig_title(
1212
+ f'Selected data and HKLs used in fitting {detector_name}')
822
1213
 
823
1214
  else:
824
1215
 
@@ -828,6 +1219,8 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
828
1219
  change_fig_title(
829
1220
  f'Select data and HKLs to use in fitting {detector_name}')
830
1221
  fig.subplots_adjust(bottom=0.2)
1222
+ if ref_map is not None:
1223
+ position_cax()
831
1224
 
832
1225
  # Setup "Add span" button
833
1226
  add_span_btn = Button(plt.axes([0.125, 0.05, 0.15, 0.075]), 'Add span')
@@ -860,6 +1253,14 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
860
1253
  reset_btn.ax.remove()
861
1254
  plt.subplots_adjust(bottom=0.0)
862
1255
 
1256
+ if filename is not None:
1257
+ fig_title[0].set_in_layout(True)
1258
+ fig.tight_layout(rect=(0, 0, 0.9, 0.9))
1259
+ if ref_map is not None:
1260
+ position_cax()
1261
+ fig.savefig(filename)
1262
+ plt.close()
1263
+
863
1264
  selected_bin_ranges = [np.searchsorted(x, span.extents).tolist()
864
1265
  for span in spans]
865
1266
  if not selected_bin_ranges:
@@ -869,7 +1270,340 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
869
1270
  else:
870
1271
  selected_hkl_indices = None
871
1272
 
872
- fig_title[0].set_in_layout(True)
873
- fig.tight_layout(rect=(0, 0, 1, 0.95))
1273
+ return selected_bin_ranges, selected_hkl_indices
1274
+
1275
+
1276
+ def get_rolling_sum_spectra(
1277
+ y, bin_axis, start=0, end=None, width=None, stride=None, num=None,
1278
+ mode='valid'):
1279
+ """
1280
+ Return the rolling sum of the spectra over a specified axis.
1281
+ """
1282
+ y = np.asarray(y)
1283
+ if not 0 <= bin_axis < y.ndim-1:
1284
+ raise ValueError(f'Invalid "bin_axis" parameter ({bin_axis})')
1285
+ size = y.shape[bin_axis]
1286
+ if not 0 <= start < size:
1287
+ raise ValueError(f'Invalid "start" parameter ({start})')
1288
+ if end is None:
1289
+ end = size
1290
+ elif not start < end <= size:
1291
+ raise ValueError('Invalid "start" and "end" combination '
1292
+ f'({start} and {end})')
1293
+
1294
+ size = end-start
1295
+ if stride is None:
1296
+ if width is None:
1297
+ width = max(1, int(size/num))
1298
+ stride = width
1299
+ else:
1300
+ width = max(1, min(width, size))
1301
+ if num is None:
1302
+ stride = width
1303
+ else:
1304
+ stride = max(1, int((size-width) / (num-1)))
1305
+ else:
1306
+ stride = max(1, min(stride, size-stride))
1307
+ if width is None:
1308
+ width = stride
1309
+ if mode == 'valid':
1310
+ num = 1 + max(0, int((size-width) / stride))
1311
+ else:
1312
+ num = int(size/stride)
1313
+ if num*stride < size:
1314
+ num += 1
1315
+ bin_ranges = [(start+n*stride, min(start+size, start+n*stride+width))
1316
+ for n in range(num)]
1317
+
1318
+ y_shape = y.shape
1319
+ y_ndim = y.ndim
1320
+ swap_axis = False
1321
+ if y_ndim > 2 and bin_axis != y_ndim-2:
1322
+ y = np.swapaxes(y, bin_axis, y_ndim-2)
1323
+ swap_axis = True
1324
+ if y_ndim > 3:
1325
+ map_shape = y.shape[0:y_ndim-2]
1326
+ y = y.reshape((np.prod(map_shape), *y.shape[y_ndim-2:]))
1327
+ if y_ndim == 2:
1328
+ y = np.expand_dims(y, 0)
1329
+
1330
+ ry = np.zeros((y.shape[0], num, y.shape[-1]), dtype=y.dtype)
1331
+ for dim in range(y.shape[0]):
1332
+ for n in range(num):
1333
+ ry[dim, n] = np.sum(y[dim,bin_ranges[n][0]:bin_ranges[n][1]], 0)
1334
+
1335
+ if y_ndim > 3:
1336
+ ry = np.reshape(ry, (*map_shape, num, y_shape[-1]))
1337
+ if y_ndim == 2:
1338
+ ry = np.squeeze(ry)
1339
+ if swap_axis:
1340
+ ry = np.swapaxes(ry, bin_axis, y_ndim-2)
1341
+
1342
+ return ry
1343
+
1344
+
1345
+ def get_spectra_fits(spectra, energies, peak_locations, detector):
1346
+ """Return twenty arrays of fit results for the map of spectra
1347
+ provided: uniform centers, uniform center errors, uniform
1348
+ amplitudes, uniform amplitude errors, uniform sigmas, uniform
1349
+ sigma errors, uniform best fit, uniform residuals, uniform reduced
1350
+ chi, uniform success codes, unconstrained centers, unconstrained
1351
+ center errors, unconstrained amplitudes, unconstrained amplitude
1352
+ errors, unconstrained sigmas, unconstrained sigma errors,
1353
+ unconstrained best fit, unconstrained residuals, unconstrained
1354
+ reduced chi, and unconstrained success codes.
1355
+
1356
+ :param spectra: Array of intensity spectra to fit.
1357
+ :type spectra: numpy.ndarray
1358
+ :param energies: Bin energies for the spectra provided.
1359
+ :type energies: numpy.ndarray
1360
+ :param peak_locations: Initial guesses for peak ceneters to use
1361
+ for the uniform fit.
1362
+ :type peak_locations: list[float]
1363
+ :param detector: A single MCA detector element configuration.
1364
+ :type detector: CHAP.edd.models.MCAElementStrainAnalysisConfig
1365
+ :returns: Uniform and unconstrained centers, amplitdues, sigmas
1366
+ (and errors for all three), best fits, residuals between the
1367
+ best fits and the input spectra, reduced chi, and fit success
1368
+ statuses.
1369
+ :rtype: tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray,
1370
+ numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray,
1371
+ numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray,
1372
+ numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray,
1373
+ numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray,
1374
+ numpy.ndarray]
1375
+ """
1376
+ # Third party modules
1377
+ from nexusformat.nexus import NXdata, NXfield
874
1378
 
875
- return fig, selected_bin_ranges, selected_hkl_indices
1379
+ # Local modules
1380
+ from CHAP.utils.fit import FitProcessor
1381
+
1382
+ num_proc = detector.num_proc
1383
+ rel_height_cutoff = detector.rel_height_cutoff
1384
+ num_peak = len(peak_locations)
1385
+ nxdata = NXdata(NXfield(spectra, 'y'), NXfield(energies, 'x'))
1386
+
1387
+ # Construct the fit model
1388
+ models = []
1389
+ if detector.background is not None:
1390
+ if isinstance(detector.background, str):
1391
+ models.append(
1392
+ {'model': detector.background, 'prefix': 'bkgd_'})
1393
+ else:
1394
+ for model in detector.background:
1395
+ models.append({'model': model, 'prefix': f'{model}_'})
1396
+ models.append(
1397
+ {'model': 'multipeak', 'centers': list(peak_locations),
1398
+ 'fit_type': 'uniform', 'peak_models': detector.peak_models,
1399
+ 'centers_range': detector.centers_range,
1400
+ 'fwhm_min': detector.fwhm_min, 'fwhm_max': detector.fwhm_max})
1401
+ config = {
1402
+ # 'code': 'lmfit',
1403
+ 'models': models,
1404
+ # 'plot': True,
1405
+ 'num_proc': num_proc,
1406
+ 'rel_height_cutoff': rel_height_cutoff,
1407
+ # 'method': 'trf',
1408
+ 'method': 'leastsq',
1409
+ # 'method': 'least_squares',
1410
+ }
1411
+
1412
+ # Perform uniform fit
1413
+ fit = FitProcessor()
1414
+ uniform_fit = fit.process(nxdata, config)
1415
+ uniform_success = uniform_fit.success
1416
+ if spectra.ndim == 1:
1417
+ if uniform_success:
1418
+ if num_peak == 1:
1419
+ uniform_fit_centers = [uniform_fit.best_values['center']]
1420
+ uniform_fit_centers_errors = [
1421
+ uniform_fit.best_errors['center']]
1422
+ uniform_fit_amplitudes = [
1423
+ uniform_fit.best_values['amplitude']]
1424
+ uniform_fit_amplitudes_errors = [
1425
+ uniform_fit.best_errors['amplitude']]
1426
+ uniform_fit_sigmas = [uniform_fit.best_values['sigma']]
1427
+ uniform_fit_centers_errors = [uniform_fit.best_errors['sigma']]
1428
+ else:
1429
+ uniform_fit_centers = [
1430
+ uniform_fit.best_values[
1431
+ f'peak{i+1}_center'] for i in range(num_peak)]
1432
+ uniform_fit_centers_errors = [
1433
+ uniform_fit.best_errors[
1434
+ f'peak{i+1}_center'] for i in range(num_peak)]
1435
+ uniform_fit_amplitudes = [
1436
+ uniform_fit.best_values[
1437
+ f'peak{i+1}_amplitude'] for i in range(num_peak)]
1438
+ uniform_fit_amplitudes_errors = [
1439
+ uniform_fit.best_errors[
1440
+ f'peak{i+1}_amplitude'] for i in range(num_peak)]
1441
+ uniform_fit_sigmas = [
1442
+ uniform_fit.best_values[
1443
+ f'peak{i+1}_sigma'] for i in range(num_peak)]
1444
+ uniform_fit_sigmas_errors = [
1445
+ uniform_fit.best_errors[
1446
+ f'peak{i+1}_sigma'] for i in range(num_peak)]
1447
+ else:
1448
+ uniform_fit_centers = list(peak_locations)
1449
+ uniform_fit_centers_errors = [0]
1450
+ uniform_fit_amplitudes = [0]
1451
+ uniform_fit_amplitudes_errors = [0]
1452
+ uniform_fit_sigmas = [0]
1453
+ uniform_fit_sigmas_errors = [0]
1454
+ else:
1455
+ if num_peak == 1:
1456
+ uniform_fit_centers = [
1457
+ uniform_fit.best_values[
1458
+ uniform_fit.best_parameters().index('center')]]
1459
+ uniform_fit_centers_errors = [
1460
+ uniform_fit.best_errors[
1461
+ uniform_fit.best_parameters().index('center')]]
1462
+ uniform_fit_amplitudes = [
1463
+ uniform_fit.best_values[
1464
+ uniform_fit.best_parameters().index('amplitude')]]
1465
+ uniform_fit_amplitudes_errors = [
1466
+ uniform_fit.best_errors[
1467
+ uniform_fit.best_parameters().index('amplitude')]]
1468
+ uniform_fit_sigmas = [
1469
+ uniform_fit.best_values[
1470
+ uniform_fit.best_parameters().index('sigma')]]
1471
+ uniform_fit_sigmas_errors = [
1472
+ uniform_fit.best_errors[
1473
+ uniform_fit.best_parameters().index('sigma')]]
1474
+ else:
1475
+ uniform_fit_centers = [
1476
+ uniform_fit.best_values[
1477
+ uniform_fit.best_parameters().index(f'peak{i+1}_center')]
1478
+ for i in range(num_peak)]
1479
+ uniform_fit_centers_errors = [
1480
+ uniform_fit.best_errors[
1481
+ uniform_fit.best_parameters().index(f'peak{i+1}_center')]
1482
+ for i in range(num_peak)]
1483
+ uniform_fit_amplitudes = [
1484
+ uniform_fit.best_values[
1485
+ uniform_fit.best_parameters().index(
1486
+ f'peak{i+1}_amplitude')]
1487
+ for i in range(num_peak)]
1488
+ uniform_fit_amplitudes_errors = [
1489
+ uniform_fit.best_errors[
1490
+ uniform_fit.best_parameters().index(
1491
+ f'peak{i+1}_amplitude')]
1492
+ for i in range(num_peak)]
1493
+ uniform_fit_sigmas = [
1494
+ uniform_fit.best_values[
1495
+ uniform_fit.best_parameters().index(f'peak{i+1}_sigma')]
1496
+ for i in range(num_peak)]
1497
+ uniform_fit_sigmas_errors = [
1498
+ uniform_fit.best_errors[
1499
+ uniform_fit.best_parameters().index(f'peak{i+1}_sigma')]
1500
+ for i in range(num_peak)]
1501
+ if not np.asarray(uniform_success).all():
1502
+ for n in range(num_peak):
1503
+ uniform_fit_centers[n] = np.where(
1504
+ uniform_success, uniform_fit_centers[n], peak_locations[n])
1505
+ uniform_fit_centers_errors[n] *= uniform_success
1506
+ uniform_fit_amplitudes[n] *= uniform_success
1507
+ uniform_fit_amplitudes_errors[n] *= uniform_success
1508
+ uniform_fit_sigmas[n] *= uniform_success
1509
+ uniform_fit_sigmas_errors[n] *= uniform_success
1510
+ uniform_best_fit = uniform_fit.best_fit
1511
+ uniform_residuals = uniform_fit.residual
1512
+ uniform_redchi = uniform_fit.redchi
1513
+
1514
+ if num_peak == 1:
1515
+ return (
1516
+ uniform_fit_centers, uniform_fit_centers_errors,
1517
+ uniform_fit_amplitudes, uniform_fit_amplitudes_errors,
1518
+ uniform_fit_sigmas, uniform_fit_sigmas_errors,
1519
+ uniform_best_fit, uniform_residuals, uniform_redchi, uniform_success,
1520
+ uniform_fit_centers, uniform_fit_centers_errors,
1521
+ uniform_fit_amplitudes, uniform_fit_amplitudes_errors,
1522
+ uniform_fit_sigmas, uniform_fit_sigmas_errors,
1523
+ uniform_best_fit, uniform_residuals,
1524
+ uniform_redchi, uniform_success
1525
+ )
1526
+
1527
+ # Perform unconstrained fit
1528
+ config['models'][-1]['fit_type'] = 'unconstrained'
1529
+ unconstrained_fit = fit.process(uniform_fit, config)
1530
+ unconstrained_success = unconstrained_fit.success
1531
+ if spectra.ndim == 1:
1532
+ if unconstrained_success:
1533
+ unconstrained_fit_centers = [
1534
+ unconstrained_fit.best_values[
1535
+ f'peak{i+1}_center'] for i in range(num_peak)]
1536
+ unconstrained_fit_centers_errors = [
1537
+ unconstrained_fit.best_errors[
1538
+ f'peak{i+1}_center'] for i in range(num_peak)]
1539
+ unconstrained_fit_amplitudes = [
1540
+ unconstrained_fit.best_values[
1541
+ f'peak{i+1}_amplitude'] for i in range(num_peak)]
1542
+ unconstrained_fit_amplitudes_errors = [
1543
+ unconstrained_fit.best_errors[
1544
+ f'peak{i+1}_amplitude'] for i in range(num_peak)]
1545
+ unconstrained_fit_sigmas = [
1546
+ unconstrained_fit.best_values[
1547
+ f'peak{i+1}_sigma'] for i in range(num_peak)]
1548
+ unconstrained_fit_sigmas_errors = [
1549
+ unconstrained_fit.best_errors[
1550
+ f'peak{i+1}_sigma'] for i in range(num_peak)]
1551
+ else:
1552
+ unconstrained_fit_centers = list(peak_locations)
1553
+ unconstrained_fit_centers_errors = [0]
1554
+ unconstrained_fit_amplitudes = [0]
1555
+ unconstrained_fit_amplitudes_errors = [0]
1556
+ unconstrained_fit_sigmas = [0]
1557
+ unconstrained_fit_sigmas_errors = [0]
1558
+ else:
1559
+ unconstrained_fit_centers = np.array(
1560
+ [unconstrained_fit.best_values[
1561
+ unconstrained_fit.best_parameters().index(f'peak{i+1}_center')]
1562
+ for i in range(num_peak)])
1563
+ unconstrained_fit_centers_errors = np.array(
1564
+ [unconstrained_fit.best_errors[
1565
+ unconstrained_fit.best_parameters().index(f'peak{i+1}_center')]
1566
+ for i in range(num_peak)])
1567
+ unconstrained_fit_amplitudes = [
1568
+ unconstrained_fit.best_values[
1569
+ unconstrained_fit.best_parameters().index(
1570
+ f'peak{i+1}_amplitude')]
1571
+ for i in range(num_peak)]
1572
+ unconstrained_fit_amplitudes_errors = [
1573
+ unconstrained_fit.best_errors[
1574
+ unconstrained_fit.best_parameters().index(
1575
+ f'peak{i+1}_amplitude')]
1576
+ for i in range(num_peak)]
1577
+ unconstrained_fit_sigmas = [
1578
+ unconstrained_fit.best_values[
1579
+ unconstrained_fit.best_parameters().index(f'peak{i+1}_sigma')]
1580
+ for i in range(num_peak)]
1581
+ unconstrained_fit_sigmas_errors = [
1582
+ unconstrained_fit.best_errors[
1583
+ unconstrained_fit.best_parameters().index(f'peak{i+1}_sigma')]
1584
+ for i in range(num_peak)]
1585
+ if not np.asarray(unconstrained_success).all():
1586
+ for n in range(num_peak):
1587
+ unconstrained_fit_centers[n] = np.where(
1588
+ unconstrained_success, unconstrained_fit_centers[n],
1589
+ peak_locations[n])
1590
+ unconstrained_fit_centers_errors[n] *= unconstrained_success
1591
+ unconstrained_fit_amplitudes[n] *= unconstrained_success
1592
+ unconstrained_fit_amplitudes_errors[n] *= unconstrained_success
1593
+ unconstrained_fit_sigmas[n] *= unconstrained_success
1594
+ unconstrained_fit_sigmas_errors[n] *= unconstrained_success
1595
+ unconstrained_best_fit = unconstrained_fit.best_fit
1596
+ unconstrained_residuals = unconstrained_fit.residual
1597
+ unconstrained_redchi = unconstrained_fit.redchi
1598
+
1599
+ return (
1600
+ uniform_fit_centers, uniform_fit_centers_errors,
1601
+ uniform_fit_amplitudes, uniform_fit_amplitudes_errors,
1602
+ uniform_fit_sigmas, uniform_fit_sigmas_errors,
1603
+ uniform_best_fit, uniform_residuals, uniform_redchi, uniform_success,
1604
+ unconstrained_fit_centers, unconstrained_fit_centers_errors,
1605
+ unconstrained_fit_amplitudes, unconstrained_fit_amplitudes_errors,
1606
+ unconstrained_fit_sigmas, unconstrained_fit_sigmas_errors,
1607
+ unconstrained_best_fit, unconstrained_residuals,
1608
+ unconstrained_redchi, unconstrained_success
1609
+ )