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

Files changed (38) hide show
  1. CHAP/__init__.py +1 -1
  2. CHAP/common/__init__.py +13 -0
  3. CHAP/common/models/integration.py +29 -26
  4. CHAP/common/models/map.py +395 -224
  5. CHAP/common/processor.py +1725 -93
  6. CHAP/common/reader.py +265 -28
  7. CHAP/common/writer.py +191 -18
  8. CHAP/edd/__init__.py +9 -2
  9. CHAP/edd/models.py +886 -665
  10. CHAP/edd/processor.py +2592 -936
  11. CHAP/edd/reader.py +889 -0
  12. CHAP/edd/utils.py +846 -292
  13. CHAP/foxden/__init__.py +6 -0
  14. CHAP/foxden/processor.py +42 -0
  15. CHAP/foxden/writer.py +65 -0
  16. CHAP/giwaxs/__init__.py +8 -0
  17. CHAP/giwaxs/models.py +100 -0
  18. CHAP/giwaxs/processor.py +520 -0
  19. CHAP/giwaxs/reader.py +5 -0
  20. CHAP/giwaxs/writer.py +5 -0
  21. CHAP/pipeline.py +48 -10
  22. CHAP/runner.py +161 -72
  23. CHAP/tomo/models.py +31 -29
  24. CHAP/tomo/processor.py +169 -118
  25. CHAP/utils/__init__.py +1 -0
  26. CHAP/utils/fit.py +1292 -1315
  27. CHAP/utils/general.py +411 -53
  28. CHAP/utils/models.py +594 -0
  29. CHAP/utils/parfile.py +10 -2
  30. ChessAnalysisPipeline-0.0.16.dist-info/LICENSE +60 -0
  31. {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.16.dist-info}/METADATA +1 -1
  32. ChessAnalysisPipeline-0.0.16.dist-info/RECORD +62 -0
  33. {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.16.dist-info}/WHEEL +1 -1
  34. CHAP/utils/scanparsers.py +0 -1431
  35. ChessAnalysisPipeline-0.0.14.dist-info/LICENSE +0 -21
  36. ChessAnalysisPipeline-0.0.14.dist-info/RECORD +0 -54
  37. {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.16.dist-info}/entry_points.txt +0 -0
  38. {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.16.dist-info}/top_level.txt +0 -0
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`.
135
+ :type interactive: bool, optional
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
132
139
  :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
+ :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,13 +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=[], label='Reference Data',
262
- interactive=False):
276
+ def select_material_params_old(x, y, tth, materials=[], label='Reference Data',
277
+ interactive=False, filename=None):
263
278
  """Interactively select the lattice parameters and space group for
264
279
  a list of materials. A matplotlib figure will be shown with a plot
265
280
  of the reference data (`x` and `y`). The figure will contain
@@ -280,17 +295,19 @@ def select_material_params(x, y, tth, materials=[], label='Reference Data',
280
295
  :param label: Legend label for the 1D plot of reference MCA data
281
296
  from the parameters `x`, `y`, defaults to `"Reference Data"`
282
297
  :type label: str, optional
283
- :param interactive: Allows for user interactions, defaults to
284
- `False`.
298
+ :param interactive: Show the plot and allow user interactions with
299
+ the matplotlib figure, defaults to `False`.
285
300
  :type interactive: bool, optional
286
- :return: A saveable matplotlib figure and the selected materials
287
- for the strain analyses.
288
- :rtype: matplotlib.figure.Figure,
289
- 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]
290
306
  """
291
307
  # Third party modules
292
- import matplotlib.pyplot as plt
293
- 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
294
311
 
295
312
  # Local modules
296
313
  from CHAP.edd.models import MaterialConfig
@@ -443,65 +460,323 @@ def select_material_params(x, y, tth, materials=[], label='Reference Data',
443
460
  widget_callbacks = []
444
461
  error_texts = []
445
462
 
446
- error_pos = (0.5, 0.95)
447
- error_props = {'fontsize': 'x-large', 'ha': 'center', 'va': 'bottom'}
448
-
449
463
  _materials = deepcopy(materials)
450
464
  for i, m in enumerate(_materials):
451
465
  if isinstance(m, MaterialConfig):
452
466
  _materials[i] = m._material
453
467
 
454
- # 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
455
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)
456
642
 
457
- 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())
458
657
 
459
- plt.subplots_adjust(bottom=0.1)
658
+ if not interactive:
460
659
 
461
- # Setup "Add material" button
462
- add_material_btn = Button(
463
- plt.axes([0.125, 0.015, 0.1, 0.05]), 'Add material')
464
- add_material_cid = add_material_btn.on_clicked(add_material)
465
- 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))
466
667
 
467
- # Setup "Confirm" button
468
- confirm_btn = Button(plt.axes([0.75, 0.015, 0.1, 0.05]), 'Confirm')
469
- confirm_cid = confirm_btn.on_clicked(confirm)
470
- widget_callbacks.append([(confirm_btn, confirm_cid)])
668
+ else:
471
669
 
472
- # Setup material-property-editing buttons for each material
473
- for material in _materials:
474
- 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))
475
700
 
476
- # Show figure for user interaction
477
701
  plt.show()
478
702
 
479
703
  # Disconnect all widget callbacks when figure is closed
480
704
  # and remove the buttons before returning the figure
481
- for group in widget_callbacks:
482
- for widget, callback in group:
483
- widget.disconnect(callback)
484
- widget.ax.remove()
485
- else:
486
- draw_plot()
487
-
488
- fig.tight_layout()
489
-
490
- 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 [
491
767
  MaterialConfig(
492
768
  material_name=m.name, sgnum=m.sgnum,
493
769
  lattice_parameters=[
494
770
  m.latticeParameters[i].value for i in range(6)])
495
- for m in _materials]
771
+ for m in materials]
496
772
 
497
- return fig, new_materials
498
773
 
499
774
  def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
500
- preselected_hkl_indices=[], detector_name=None, ref_map=None,
501
- flux_energy_range=None, calibration_bin_ranges=None,
502
- label='Reference Data', 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):
503
778
  """Return a matplotlib figure to indicate data ranges and HKLs to
504
- include for fitting in EDD Ceria calibration and/or strain
779
+ include for fitting in EDD energy/tth calibration and/or strain
505
780
  analysis.
506
781
 
507
782
  :param x: MCA channel energies.
@@ -523,6 +798,9 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
523
798
  :param preselected_hkl_indices: Preselected unique HKL indices to
524
799
  fit peaks for in the calibration routine, defaults to `[]`.
525
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
526
804
  :param detector_name: Name of the MCA detector element.
527
805
  :type detector_name: str, optional
528
806
  :param ref_map: Reference map of MCA intensities to show underneath
@@ -534,28 +812,32 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
534
812
  :param calibration_bin_ranges: MCA channel index ranges included
535
813
  in the detector calibration.
536
814
  :type calibration_bin_ranges: list[[int, int]], optional
537
- :param interactive: Allows for user interactions, defaults to
538
- `False`.
539
- :type interactive: bool, optional
540
815
  :param label: Legend label for the 1D plot of reference MCA data
541
816
  from the parameters `x`, `y`, defaults to `"Reference Data"`
542
817
  :type label: str, optional
543
- :return: A saveable matplotlib figure, the list of selected data
544
- index ranges to include, and the list of HKL indices to
545
- include
546
- :rtype: matplotlib.figure.Figure, list[list[int]], list[int]
818
+ :param interactive: Show the plot and allow user interactions with
819
+ the matplotlib figure, defaults to `True`.
820
+ :type interactive: bool, optional
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]
547
827
  """
548
828
  # Third party modules
549
- import matplotlib.lines as mlines
550
- 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
551
833
  import matplotlib.pyplot as plt
552
- from matplotlib.widgets import Button, SpanSelector
834
+ from matplotlib.widgets import SpanSelector
553
835
 
554
836
  # Local modules
555
837
  from CHAP.utils.general import (
556
838
  get_consecutive_int_range,
557
839
  index_nearest_down,
558
- index_nearest_upp,
840
+ index_nearest_up,
559
841
  )
560
842
 
561
843
  def change_fig_title(title):
@@ -570,10 +852,20 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
570
852
  error_texts.pop()
571
853
  error_texts.append(plt.figtext(*error_pos, error, **error_props))
572
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
+
573
865
  def hkl_locations_in_any_span(hkl_index):
574
866
  """Return the index of the span where the location of a specific
575
867
  HKL resides. Return(-1 if outside any span."""
576
- if hkl_index < 0 or hkl_index>= len(hkl_locations):
868
+ if hkl_index < 0 or hkl_index >= len(hkl_locations):
577
869
  return -1
578
870
  for i, span in enumerate(spans):
579
871
  if (span.extents[0] <= hkl_locations[hkl_index] and
@@ -581,15 +873,21 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
581
873
  return i
582
874
  return -1
583
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
+
584
882
  def on_span_select(xmin, xmax):
585
883
  """Callback function for the SpanSelector widget."""
586
884
  removed_hkls = False
587
- if not init_flag[0]:
588
- for hkl_index in deepcopy(selected_hkl_indices):
589
- 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:
590
888
  hkl_vlines[hkl_index].set(**excluded_hkl_props)
591
- selected_hkl_indices.remove(hkl_index)
592
- removed_hkls = True
889
+ selected_hkl_indices.remove(hkl_index)
890
+ removed_hkls = True
593
891
  combined_spans = False
594
892
  combined_spans_test = True
595
893
  while combined_spans_test:
@@ -617,48 +915,39 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
617
915
  for hkl_index in range(len(hkl_locations)):
618
916
  if (hkl_index not in selected_hkl_indices
619
917
  and hkl_locations_in_any_span(hkl_index) >= 0):
620
- 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)
621
920
  selected_hkl_indices.append(hkl_index)
622
921
  added_hkls = True
623
- if combined_spans:
624
- 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:
625
932
  change_error_text(
626
- 'Combined overlapping spans and selected only HKL(s) '
627
- 'inside the selected energy mask')
628
- else:
933
+ 'Adjusted the selected HKL(s) to match the selected '
934
+ 'energy mask')
935
+ elif added_hkls:
629
936
  change_error_text(
630
- 'Combined overlapping spans in the selected energy mask')
631
- elif added_hkls and removed_hkls:
632
- change_error_text(
633
- 'Adjusted the selected HKL(s) to match the selected '
634
- 'energy mask')
635
- elif added_hkls:
636
- change_error_text(
637
- 'Added HKL(s) to match the selected energy mask')
638
- elif removed_hkls:
639
- change_error_text(
640
- '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')
641
941
  # If using ref_map, update the colorbar range to min / max of
642
942
  # the selected data only
643
943
  if ref_map is not None:
644
944
  selected_data = ref_map[:,get_mask()]
645
- _min, _max = np.argmin(selected_data), np.argmax(selected_data)
646
945
  ref_map_mappable = ax_map.pcolormesh(
647
- x, np.arange(ref_map.shape[0]), ref_map, vmin=_min, vmax=_max)
946
+ x, np.arange(ref_map.shape[0]), ref_map,
947
+ vmin=selected_data.min(), vmax=selected_data.max())
648
948
  fig.colorbar(ref_map_mappable, cax=cax)
649
949
  plt.draw()
650
950
 
651
- def get_mask():
652
- """Return a boolean array that acts as the mask corresponding
653
- to the currently-selected index ranges"""
654
- mask = np.full(x.shape[0], False)
655
- bin_indices = np.arange(x.shape[0])
656
- for span in spans:
657
- _min, _max = span.extents
658
- mask = np.logical_or(
659
- mask, np.logical_and(bin_indices >= _min, bin_indices <= _max))
660
- return mask
661
-
662
951
  def add_span(event, xrange_init=None):
663
952
  """Callback function for the "Add span" button."""
664
953
  spans.append(
@@ -688,17 +977,19 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
688
977
  hkl_vline.set(**excluded_hkl_props)
689
978
  selected_hkl_indices.remove(hkl_index)
690
979
  span = spans[hkl_locations_in_any_span(hkl_index)]
691
- span_next_hkl_index = hkl_locations_in_any_span(hkl_index+1)
692
980
  span_prev_hkl_index = hkl_locations_in_any_span(hkl_index-1)
693
- 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):
694
985
  span.set_visible(False)
695
986
  spans.remove(span)
696
- elif span_next_hkl_index < 0:
987
+ elif span_curr_hkl_index != span_next_hkl_index:
697
988
  span.extents = (
698
989
  span.extents[0],
699
990
  0.5*(hkl_locations[hkl_index-1]
700
991
  + hkl_locations[hkl_index]))
701
- elif span_prev_hkl_index < 0:
992
+ elif span_curr_hkl_index != span_prev_hkl_index:
702
993
  span.extents = (
703
994
  0.5*(hkl_locations[hkl_index]
704
995
  + hkl_locations[hkl_index+1]),
@@ -717,19 +1008,54 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
717
1008
  f'Adjusted the selected energy mask to reflect the '
718
1009
  'removed HKL')
719
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_))
720
1052
  change_error_text(
721
- f'Selected HKL is outside any current span, '
722
- 'extend or add spans before adding this value')
1053
+ f'Adjusted the selected energy mask to reflect the '
1054
+ 'added HKL')
723
1055
  plt.draw()
724
1056
 
725
- def position_cax():
726
- """Reposition the colorbar axes according to the axes of the
727
- reference map"""
728
- ((left, bottom), (right, top)) = ax_map.get_position().get_points()
729
- cax.set_position([right + 0.01, bottom, 0.01, top - bottom])
730
-
731
1057
  def reset(event):
732
- """Callback function for the "Confirm" button."""
1058
+ """Callback function for the "Reset" button."""
733
1059
  for hkl_index in deepcopy(selected_hkl_indices):
734
1060
  hkl_vlines[hkl_index].set(**excluded_hkl_props)
735
1061
  selected_hkl_indices.remove(hkl_index)
@@ -740,8 +1066,9 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
740
1066
 
741
1067
  def confirm(event):
742
1068
  """Callback function for the "Confirm" button."""
743
- if not len(spans) or len(selected_hkl_indices) < 2:
744
- 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')
745
1072
  plt.draw()
746
1073
  else:
747
1074
  if error_texts:
@@ -760,78 +1087,12 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
760
1087
  fig_title = []
761
1088
  error_texts = []
762
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
763
1094
  hkl_locations = [loc for loc in get_peak_locations(ds, tth)
764
1095
  if x[0] <= loc <= x[-1]]
765
- hkl_labels = [str(hkl)[1:-1] for hkl, loc in zip(hkls, hkl_locations)]
766
-
767
- title_pos = (0.5, 0.95)
768
- title_props = {'fontsize': 'xx-large', 'ha': 'center', 'va': 'bottom'}
769
- error_pos = (0.5, 0.90)
770
- error_props = {'fontsize': 'x-large', 'ha': 'center', 'va': 'bottom'}
771
- excluded_hkl_props = {
772
- 'color': 'black', 'linestyle': '--','linewidth': 1,
773
- 'marker': 10, 'markersize': 5, 'fillstyle': 'none'}
774
- included_hkl_props = {
775
- 'color': 'green', 'linestyle': '-', 'linewidth': 2,
776
- 'marker': 10, 'markersize': 10, 'fillstyle': 'full'}
777
- excluded_data_props = {
778
- 'facecolor': 'white', 'edgecolor': 'gray', 'linestyle': ':'}
779
- included_data_props = {
780
- 'alpha': 0.5, 'facecolor': 'tab:blue', 'edgecolor': 'blue'}
781
-
782
- if flux_energy_range is None:
783
- min_x = x.min()
784
- max_x = x.max()
785
- else:
786
- min_x = x[index_nearest_upp(x, max(x.min(), flux_energy_range[0]))]
787
- max_x = x[index_nearest_down(x, min(x.max(), flux_energy_range[1]))]
788
-
789
- if ref_map is None:
790
- fig, ax = plt.subplots(figsize=(11, 8.5))
791
- ax.set(xlabel='Energy (keV)', ylabel='Intensity (counts)')
792
- else:
793
- # Ensure ref_map is 2D
794
- if ref_map.ndim > 2:
795
- ref_map = np.reshape(
796
- ref_map, (np.prod(ref_map.shape[:-1]), ref_map.shape[-1]))
797
- # If needed, abbreviate ref_map to <= 50 spectra to keep
798
- # response time of mouse interactions quick.
799
- max_ref_spectra = 50
800
- if ref_map.shape[0] > max_ref_spectra:
801
- choose_i = np.sort(
802
- np.random.choice(
803
- ref_map.shape[0], max_ref_spectra, replace=False))
804
- ref_map = ref_map[choose_i]
805
- fig, (ax, ax_map) = plt.subplots(
806
- 2, sharex=True, figsize=(11, 8.5), height_ratios=[2, 1])
807
- ax.set(ylabel='Intensity (counts)')
808
- ref_map_mappable = ax_map.pcolormesh(
809
- x, np.arange(ref_map.shape[0]), ref_map)
810
- ax_map.set_yticks([])
811
- ax_map.set_xlabel('Energy (keV)')
812
- ax_map.set_xlim(x[0], x[-1])
813
- ((left, bottom), (right, top)) = ax_map.get_position().get_points()
814
- cax = plt.axes([right + 0.01, bottom, 0.01, top - bottom])
815
- fig.colorbar(ref_map_mappable, cax=cax)
816
- handles = ax.plot(x, y, color='k', label=label)
817
- if calibration_bin_ranges is not None:
818
- ylow = ax.get_ylim()[0]
819
- for low, upp in calibration_bin_ranges:
820
- ax.plot([x[low], x[upp]], [ylow, ylow], color='r', linewidth=2)
821
- handles.append(mlines.Line2D(
822
- [], [], label='Energies included in calibration', color='r',
823
- linewidth=2))
824
- handles.append(mlines.Line2D(
825
- [], [], label='Excluded / unselected HKL', **excluded_hkl_props))
826
- handles.append(mlines.Line2D(
827
- [], [], label='Included / selected HKL', **included_hkl_props))
828
- handles.append(Patch(
829
- label='Excluded / unselected data', **excluded_data_props))
830
- handles.append(Patch(
831
- label='Included / selected data', **included_data_props))
832
- ax.legend(handles=handles)
833
- ax.set_xlim(x[0], x[-1])
834
-
835
1096
  if selected_hkl_indices and not preselected_bin_ranges:
836
1097
  index_ranges = get_consecutive_int_range(selected_hkl_indices)
837
1098
  for index_range in index_ranges:
@@ -846,30 +1107,109 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
846
1107
  else:
847
1108
  max_ = 0.5*(hkl_locations[j] + max_x)
848
1109
  preselected_bin_ranges.append(
849
- [index_nearest_upp(x, min_), index_nearest_down(x, max_)])
1110
+ [index_nearest_up(x, min_), index_nearest_down(x, max_)])
850
1111
 
851
- for i, (loc, lbl) in enumerate(zip(hkl_locations, hkl_labels)):
852
- nearest_index = np.searchsorted(x, loc)
853
- if i in selected_hkl_indices:
854
- hkl_vline = ax.axvline(loc, **included_hkl_props)
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()
1126
+
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)')
855
1147
  else:
856
- hkl_vline = ax.axvline(loc, **excluded_hkl_props)
857
- ax.text(loc, 1, lbl, ha='right', va='top', rotation=90,
858
- transform=ax.get_xaxis_transform())
859
- 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])
860
1188
 
861
- 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
862
1202
  for bin_range in preselected_bin_ranges:
863
1203
  add_span(None, xrange_init=x[bin_range])
864
- init_flag = [False]
865
1204
 
866
1205
  if not interactive:
867
1206
 
868
- if detector_name is None:
869
- change_fig_title('Selected data and HKLs used in fitting')
870
- else:
871
- change_fig_title(
872
- 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}')
873
1213
 
874
1214
  else:
875
1215
 
@@ -913,6 +1253,14 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
913
1253
  reset_btn.ax.remove()
914
1254
  plt.subplots_adjust(bottom=0.0)
915
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
+
916
1264
  selected_bin_ranges = [np.searchsorted(x, span.extents).tolist()
917
1265
  for span in spans]
918
1266
  if not selected_bin_ranges:
@@ -922,15 +1270,79 @@ def select_mask_and_hkls(x, y, hkls, ds, tth, preselected_bin_ranges=[],
922
1270
  else:
923
1271
  selected_hkl_indices = None
924
1272
 
925
- fig_title[0].set_in_layout(True)
926
- fig.tight_layout(rect=(0, 0, 0.9, 0.9))
927
- if ref_map is not None:
928
- position_cax()
929
-
930
- return fig, selected_bin_ranges, selected_hkl_indices
1273
+ return selected_bin_ranges, selected_hkl_indices
931
1274
 
932
1275
 
933
- def get_spectra_fits(spectra, energies, peak_locations, fit_params):
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):
934
1346
  """Return twenty arrays of fit results for the map of spectra
935
1347
  provided: uniform centers, uniform center errors, uniform
936
1348
  amplitudes, uniform amplitude errors, uniform sigmas, uniform
@@ -948,8 +1360,8 @@ def get_spectra_fits(spectra, energies, peak_locations, fit_params):
948
1360
  :param peak_locations: Initial guesses for peak ceneters to use
949
1361
  for the uniform fit.
950
1362
  :type peak_locations: list[float]
951
- :param fit_params: Detector element fit parameters.
952
- :type fit_params: CHAP.edd.models.MCAElementStrainAnalysisConfig
1363
+ :param detector: A single MCA detector element configuration.
1364
+ :type detector: CHAP.edd.models.MCAElementStrainAnalysisConfig
953
1365
  :returns: Uniform and unconstrained centers, amplitdues, sigmas
954
1366
  (and errors for all three), best fits, residuals between the
955
1367
  best fits and the input spectra, reduced chi, and fit success
@@ -961,86 +1373,228 @@ def get_spectra_fits(spectra, energies, peak_locations, fit_params):
961
1373
  numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray,
962
1374
  numpy.ndarray]
963
1375
  """
964
- from CHAP.utils.fit import FitMap
1376
+ # Third party modules
1377
+ from nexusformat.nexus import NXdata, NXfield
965
1378
 
966
- # Perform fit to get measured peak positions
967
- fit = FitMap(spectra, x=energies)
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
968
1384
  num_peak = len(peak_locations)
969
- delta = 0.1 * (energies[-1]-energies[0])
970
- centers_range = (
971
- max(0.0, energies[0]-delta), energies[-1]+delta)
972
- fit.create_multipeak_model(
973
- peak_locations,
974
- fit_type='uniform',
975
- peak_models=fit_params.peak_models,
976
- background=fit_params.background,
977
- fwhm_min=fit_params.fwhm_min,
978
- fwhm_max=fit_params.fwhm_max,
979
- centers_range=centers_range)
980
- fit.fit(num_proc=fit_params.num_proc)
981
- uniform_fit_centers = [
982
- fit.best_values[
983
- fit.best_parameters().index(f'peak{i+1}_center')]
984
- for i in range(num_peak)]
985
- uniform_fit_centers_errors = [
986
- fit.best_errors[
987
- fit.best_parameters().index(f'peak{i+1}_center')]
988
- for i in range(num_peak)]
989
- uniform_fit_amplitudes = [
990
- fit.best_values[
991
- fit.best_parameters().index(f'peak{i+1}_amplitude')]
992
- for i in range(num_peak)]
993
- uniform_fit_amplitudes_errors = [
994
- fit.best_errors[
995
- fit.best_parameters().index(f'peak{i+1}_amplitude')]
996
- for i in range(num_peak)]
997
- uniform_fit_sigmas = [
998
- fit.best_values[
999
- fit.best_parameters().index(f'peak{i+1}_sigma')]
1000
- for i in range(num_peak)]
1001
- uniform_fit_sigmas_errors = [
1002
- fit.best_errors[
1003
- fit.best_parameters().index(f'peak{i+1}_sigma')]
1004
- for i in range(num_peak)]
1005
- uniform_best_fit = fit.best_fit
1006
- uniform_residuals = fit.residual
1007
- uniform_redchi = fit.redchi
1008
- uniform_success = fit.success
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
+ )
1009
1526
 
1010
1527
  # Perform unconstrained fit
1011
- fit.create_multipeak_model(fit_type='unconstrained')
1012
- fit.fit(num_proc=fit_params.num_proc,
1013
- rel_amplitude_cutoff=fit_params.rel_amplitude_cutoff)
1014
- unconstrained_fit_centers = np.array(
1015
- [fit.best_values[
1016
- fit.best_parameters()\
1017
- .index(f'peak{i+1}_center')]
1018
- for i in range(num_peak)])
1019
- unconstrained_fit_centers_errors = np.array(
1020
- [fit.best_errors[
1021
- fit.best_parameters()\
1022
- .index(f'peak{i+1}_center')]
1023
- for i in range(num_peak)])
1024
- unconstrained_fit_amplitudes = [
1025
- fit.best_values[
1026
- fit.best_parameters().index(f'peak{i+1}_amplitude')]
1027
- for i in range(num_peak)]
1028
- unconstrained_fit_amplitudes_errors = [
1029
- fit.best_errors[
1030
- fit.best_parameters().index(f'peak{i+1}_amplitude')]
1031
- for i in range(num_peak)]
1032
- unconstrained_fit_sigmas = [
1033
- fit.best_values[
1034
- fit.best_parameters().index(f'peak{i+1}_sigma')]
1035
- for i in range(num_peak)]
1036
- unconstrained_fit_sigmas_errors = [
1037
- fit.best_errors[
1038
- fit.best_parameters().index(f'peak{i+1}_sigma')]
1039
- for i in range(num_peak)]
1040
- unconstrained_best_fit = fit.best_fit
1041
- unconstrained_residuals = fit.residual
1042
- unconstrained_redchi = fit.redchi
1043
- unconstrained_success = fit.success
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
1044
1598
 
1045
1599
  return (
1046
1600
  uniform_fit_centers, uniform_fit_centers_errors,