nxs-analysis-tools 0.1.5__tar.gz → 0.1.7__tar.gz

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 nxs-analysis-tools might be problematic. Click here for more details.

Files changed (28) hide show
  1. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/PKG-INFO +1 -1
  2. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/pyproject.toml +2 -2
  3. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/src/_meta/__init__.py +1 -1
  4. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/src/nxs_analysis_tools/__init__.py +2 -1
  5. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/src/nxs_analysis_tools/chess.py +140 -14
  6. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/src/nxs_analysis_tools/datareduction.py +179 -2
  7. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/src/nxs_analysis_tools/fitting.py +51 -9
  8. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/LICENSE +0 -0
  9. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/MANIFEST.in +0 -0
  10. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/README.md +0 -0
  11. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/setup.cfg +0 -0
  12. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/setup.py +0 -0
  13. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/src/nxs_analysis_tools/pairdistribution.py +0 -0
  14. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/src/nxs_analysis_tools.egg-info/SOURCES.txt +0 -0
  15. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/tests/test_accurate_highlight.py +0 -0
  16. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/tests/test_chess.py +0 -0
  17. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/tests/test_chess_fitting.py +0 -0
  18. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/tests/test_datareduction.py +0 -0
  19. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/tests/test_ellipsoidal_window.py +0 -0
  20. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/tests/test_fitting.py +0 -0
  21. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/tests/test_lmfit.py +0 -0
  22. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/tests/test_mask_plotting.py +0 -0
  23. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/tests/test_pairdistribution.py +0 -0
  24. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/tests/test_plot_slice_axes_types.py +0 -0
  25. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/tests/test_plot_slice_with_ndarray.py +0 -0
  26. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/tests/test_rotate_data.py +0 -0
  27. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/tests/test_sum_axis.py +0 -0
  28. {nxs_analysis_tools-0.1.5 → nxs_analysis_tools-0.1.7}/tests/test_symmetrizer_rectangular_plane.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nxs-analysis-tools
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Reduce and transform nexus format (.nxs) scattering data.
5
5
  Author-email: "Steven J. Gomez Alvarado" <stevenjgomez@ucsb.edu>
6
6
  License-Expression: MIT
@@ -6,7 +6,7 @@ build-backend = 'setuptools.build_meta'
6
6
 
7
7
  [project]
8
8
  name = 'nxs-analysis-tools'
9
- version = '0.1.5'
9
+ version = '0.1.7'
10
10
  description = 'Reduce and transform nexus format (.nxs) scattering data.'
11
11
  readme = 'README.md'
12
12
  requires-python = '>=3.7'
@@ -71,7 +71,7 @@ dev = [
71
71
  'DOI' = 'https://doi.org/10.5281/zenodo.15186359'
72
72
 
73
73
  [tool.bumpver]
74
- current_version = "0.1.5"
74
+ current_version = "0.1.7"
75
75
  version_pattern = "MAJOR.MINOR.PATCH[-TAG]"
76
76
  tag_pattern = "vMAJOR.MINOR.PATCH[-TAG]"
77
77
  commit_message = "Bump version {old_version} -> {new_version}"
@@ -6,5 +6,5 @@ __author__ = 'Steven J. Gomez Alvarado'
6
6
  __email__ = 'stevenjgomez@ucsb.edu'
7
7
  __copyright__ = f"2023-2025, {__author__}"
8
8
  __license__ = 'MIT'
9
- __version__ = '0.1.5'
9
+ __version__ = '0.1.7'
10
10
  __repo_url__ = 'https://github.com/stevenjgomez/nxs_analysis_tools'
@@ -11,4 +11,5 @@ from .chess import TempDependence
11
11
  __all__ = ['load_data', 'load_transform', 'plot_slice', 'Scissors',
12
12
  'reciprocal_lattice_params', 'rotate_data', 'rotate_data_2D',
13
13
  'convert_to_inverse_angstroms', 'array_to_nxdata', 'Padder',
14
- 'rebin_nxdata', 'rebin_3d', 'rebin_1d'] + ['TempDependence']
14
+ 'rebin_nxdata', 'rebin_3d', 'rebin_1d', 'TempDependence',
15
+ 'animate_slice_temp', 'animate_slice_axis']
@@ -14,6 +14,7 @@ from IPython.display import display, Markdown
14
14
  from nxs_analysis_tools import load_data, Scissors
15
15
  from nxs_analysis_tools.fitting import LinecutModel
16
16
  from nxs_analysis_tools.datareduction import load_transform, reciprocal_lattice_params
17
+ from lmfit.models import PseudoVoigtModel, LinearModel
17
18
 
18
19
 
19
20
  class TempDependence:
@@ -98,18 +99,32 @@ class TempDependence:
98
99
  Fit the line cut models for each temperature.
99
100
  plot_fit(mdheadings=False, **kwargs):
100
101
  Plot the fit results for each temperature.
101
- plot_order_parameter(self):
102
+ overlay_fits(numpoints=None, vertical_offset=0, cmap='viridis', ax=ax):
103
+ Plot raw data and fitted models for each temperature.
104
+ fit_peak_simple():
105
+ Perform a basic fit using a pseudo-Voigt peak shape, linear background, and no constraints.
106
+ plot_order_parameter(ax, **kwargs):
102
107
  Plot the temperature dependence of the peakheight parameter.
103
108
  print_fit_report():
104
109
  Print the fit report for each temperature.
105
110
  """
106
111
 
107
- def __init__(self):
112
+ def __init__(self, sample_directory=None):
108
113
  """
109
- Initialize the TempDependence class with default values.
114
+ Initialize the TempDependence class.
115
+
116
+ Parameters
117
+ ----------
118
+ sample_directory : str, optional
119
+ Path to the directory containing the temperature folders.
120
+ If None, no directory is set initially.
110
121
  """
111
122
 
112
- self.sample_directory = None
123
+ if sample_directory is None:
124
+ self.sample_directory = None
125
+ else:
126
+ self.set_sample_directory(sample_directory)
127
+
113
128
  self.xlabel = ''
114
129
  self.datasets = {}
115
130
  self.temperatures = []
@@ -133,7 +148,7 @@ class TempDependence:
133
148
 
134
149
  def find_temperatures(self):
135
150
  """
136
- Set the list of temperatures by automatically scanning the sample directory.
151
+ Set the list of temperatures by automatically scanning the sample directory for .nxs files from nxrefine.
137
152
  """
138
153
 
139
154
  # Assert that self.sample_directory must exist
@@ -449,7 +464,7 @@ class TempDependence:
449
464
 
450
465
  Parameters
451
466
  ----------
452
- ax : matplotlib.axes.Axes, optional
467
+ ax : :class:`matplotlib.axes.Axes`, optional
453
468
  The axes on which to plot the heatmap. If None, a new figure and axes
454
469
  are created. The default is None.
455
470
  **kwargs
@@ -469,7 +484,7 @@ class TempDependence:
469
484
  y = np.array([int(t) for t in self.temperatures])
470
485
 
471
486
  # Collect counts from each temperature and ensure they are numpy arrays
472
- v = [self.linecuts[T].counts.nxdata for T in self.temperatures]
487
+ v = [self.linecuts[T].nxsignal.nxdata for T in self.temperatures]
473
488
 
474
489
  # Convert list of arrays to a 2D array for the heatmap
475
490
  v_2d = np.array(v)
@@ -548,7 +563,7 @@ class TempDependence:
548
563
 
549
564
  Parameters
550
565
  ----------
551
- model_components : Model or iterable of Model
566
+ model_components : Model, CompositeModel, or iterable of Model
552
567
  The model components to set for all line cut models.
553
568
 
554
569
  """
@@ -561,7 +576,8 @@ class TempDependence:
561
576
 
562
577
  This method sets the parameter hints for all line cut models in the analysis.
563
578
  It iterates over each line cut model and calls their respective `set_param_hint` method
564
- with the provided arguments and keyword arguments.
579
+ with the provided arguments and keyword arguments. These are implemented when the
580
+ .make_params() method is called.
565
581
 
566
582
  Parameters
567
583
  ----------
@@ -573,10 +589,40 @@ class TempDependence:
573
589
  """
574
590
  [linecutmodel.set_param_hint(*args, **kwargs)
575
591
  for linecutmodel in self.linecutmodels.values()]
592
+
593
+ def params_set(self, name, **kwargs):
594
+ """
595
+ Set constraints on a parameter for all line cut models.
596
+
597
+ This method updates the specified parameter across all models in
598
+ `self.linecutmodels` using the keyword arguments provided. These
599
+ keyword arguments are passed to the `set()` method of the parameter,
600
+ which comes from a `lmfit.Parameters` object.
601
+
602
+ Parameters
603
+ ----------
604
+ name : str
605
+ Name of the parameter to modify (must exist in each model).
606
+ **kwargs
607
+ Constraint arguments passed to `Parameter.set()`, such as `value`,
608
+ `min`, `max`, `vary`, etc.
609
+
610
+ Raises
611
+ ------
612
+ KeyError
613
+ If the parameter `name` does not exist in one of the models.
614
+
615
+ Example
616
+ -------
617
+ >>> sample.params_set('peakamplitude', value=5, min=0, vary=True)
618
+ """
619
+
620
+ for linecutmodel in self.linecutmodels.values():
621
+ linecutmodel.params[name].set(**kwargs)
576
622
 
577
623
  def make_params(self):
578
624
  """
579
- Make parameters for all line cut models.
625
+ Create and initialize the parameters for all models.
580
626
 
581
627
  This method creates the parameters for all line cut models in the analysis.
582
628
  It iterates over each line cut model and calls their respective `make_params` method.
@@ -585,7 +631,8 @@ class TempDependence:
585
631
 
586
632
  def guess(self):
587
633
  """
588
- Make initial parameter guesses for all line cut models.
634
+ Make initial parameter guesses for all line cut models. This overwrites any prior initial
635
+ values and constraints.
589
636
 
590
637
  This method generates initial parameter guesses for all line cut models in the analysis.
591
638
  It iterates over each line cut model and calls their respective `guess` method.
@@ -657,7 +704,70 @@ class TempDependence:
657
704
  title=f"{T} K",
658
705
  **kwargs)
659
706
 
660
- def plot_order_parameter(self):
707
+ def overlay_fits(self, numpoints=None, vertical_offset=0, cmap='viridis', ax=None):
708
+ """
709
+ Plot raw data and fitted models for each temperature with optional vertical offsets.
710
+
711
+ Parameters:
712
+ -----------
713
+ numpoints : int or None, default=None
714
+ Number of points to evaluate for the fitted model curves.
715
+ If None, uses the number of raw data points for each linecut.
716
+ vertical_offset : float, default=0
717
+ Amount to vertically offset each linecut for clarity.
718
+ cmap : str, default='viridis'
719
+ Name of the matplotlib colormap used to distinguish different temperatures.
720
+ ax : :class:`matplotlib.axes.Axes` or None, default=None
721
+ Axis object to plot on. If None, a new figure and axis are created.
722
+
723
+ The function:
724
+ - Uses a colormap to assign unique colors to each temperature.
725
+ - Plots raw data alongside evaluated fit models for each linecut.
726
+ - Vertically offsets each trace by a constant value for visual separation.
727
+ - Displays a legend in reverse order to match top-to-bottom visual stacking.
728
+ - Automatically labels the x- and y-axes based on NeXus-style data metadata.
729
+ """
730
+
731
+ # Create a figure and axes if an axis is not already provided
732
+ _, ax = plt.subplots() if ax is None else (None, ax)
733
+
734
+ # Generate a color palette for the various temperatures
735
+ cmap = plt.get_cmap(cmap)
736
+ colors = [cmap(i / len(self.temperatures)) for i, _ in enumerate(self.temperatures)]
737
+
738
+ for i, lm in enumerate(self.linecutmodels.values()):
739
+ # Plot the raw data
740
+ ax.plot(lm.x, lm.y + vertical_offset * i, '.', c=colors[i])
741
+
742
+ # Evaluate the fit
743
+ numpoints = len(lm.x) if numpoints is None else numpoints
744
+ x_eval = np.linspace(lm.x.min(), lm.x.max(), numpoints)
745
+ y_eval = lm.modelresult.eval(x=x_eval)
746
+ ax.plot(x_eval, y_eval + vertical_offset * i, '-', c=colors[i], label=self.temperatures[i])
747
+
748
+ # Reverse legend entries to match top-to-bottom stacking
749
+ handles, labels = ax.get_legend_handles_labels()
750
+ ax.legend(handles[::-1], labels[::-1])
751
+
752
+ # Add axis labels
753
+ ax.set(xlabel=lm.data.nxaxes[0].nxname, ylabel=lm.data.nxsignal.nxname)
754
+
755
+ def fit_peak_simple(self):
756
+ """
757
+ Fit all linecuts in the temperature series using a pseudo-Voigt peak shape and linear
758
+ background, with no constraints.
759
+ """
760
+
761
+ for T in self.temperatures:
762
+ linecutmodel = self.linecutmodels[T]
763
+ linecutmodel.set_model_components([PseudoVoigtModel(prefix='peak'),
764
+ LinearModel(prefix='background')])
765
+ linecutmodel.make_params()
766
+ linecutmodel.guess()
767
+ linecutmodel.params.set('peakamplitude', min=0)
768
+ linecutmodel.fit()
769
+
770
+ def plot_order_parameter(self, ax=None, **kwargs):
661
771
  """
662
772
  Plot the temperature dependence of the peak height (order parameter).
663
773
 
@@ -665,6 +775,14 @@ class TempDependence:
665
775
  line cut fit stored in `linecutmodels` and plots it as a function
666
776
  of temperature using matplotlib.
667
777
 
778
+ Parameters
779
+ ----------
780
+ ax : :class:`matplotlib.axes.Axes`, optional
781
+ Axis object to plot on. If None, a new figure and axis are created.
782
+ **kwargs
783
+ Keyword arguments to be passed to the plot function.
784
+
785
+
668
786
  Returns
669
787
  -------
670
788
  Figure
@@ -687,11 +805,19 @@ class TempDependence:
687
805
 
688
806
  # Extract the peakheight at every temperature
689
807
  for T in self.temperatures:
808
+
809
+ # Verify that the fit has already been completed
810
+ if self.linecutmodels[T].modelresult is None:
811
+ raise AttributeError("Model result is empty. Have you fit the data to a model?")
812
+
690
813
  peakheights.append(self.linecutmodels[T].modelresult.params['peakheight'].value)
691
814
 
692
815
  # Plot the peakheights vs. temperature
693
- fig, ax = plt.subplots()
694
- ax.plot(temperatures, peakheights)
816
+ if ax is None:
817
+ fig, ax = plt.subplots()
818
+ else:
819
+ fig = ax.figure
820
+ ax.plot(temperatures, peakheights, **kwargs)
695
821
  ax.set(xlabel='$T$ (K)', ylabel='peakheight')
696
822
  return fig, ax
697
823
 
@@ -2,22 +2,27 @@
2
2
  Reduces scattering data into 2D and 1D datasets.
3
3
  """
4
4
  import os
5
+ import io
6
+ import warnings
5
7
  import numpy as np
6
8
  import matplotlib.pyplot as plt
7
9
  from matplotlib.transforms import Affine2D
8
10
  from matplotlib.markers import MarkerStyle
9
11
  from matplotlib.ticker import MultipleLocator
12
+ import matplotlib.animation as animation
10
13
  from matplotlib import colors
11
14
  from matplotlib import patches
12
- from IPython.display import display, Markdown
15
+ from IPython.display import display, Markdown, HTML, Image
13
16
  from nexusformat.nexus import NXfield, NXdata, nxload, NeXusError, NXroot, NXentry, nxsave
14
17
  from scipy import ndimage
15
18
 
19
+
16
20
  # Specify items on which users are allowed to perform standalone imports
17
21
  __all__ = ['load_data', 'load_transform', 'plot_slice', 'Scissors',
18
22
  'reciprocal_lattice_params', 'rotate_data', 'rotate_data_2D',
19
23
  'convert_to_inverse_angstroms', 'array_to_nxdata', 'Padder',
20
- 'rebin_nxdata', 'rebin_3d', 'rebin_1d']
24
+ 'rebin_nxdata', 'rebin_3d', 'rebin_1d', 'animate_slice_temp',
25
+ 'animate_slice_axis']
21
26
 
22
27
 
23
28
  def load_data(path, print_tree=True):
@@ -407,6 +412,9 @@ def plot_slice(data, X=None, Y=None, sum_axis=None, transpose=False, vmin=None,
407
412
  # Use the 2D template to create a new nxdata
408
413
  data = array_to_nxdata(arr, data[slice_obj])
409
414
 
415
+ if data.ndim != 2:
416
+ raise ValueError("Slice data must be 2D.")
417
+
410
418
  # If the data is of type ndarray, then convert to NXdata
411
419
  if is_array:
412
420
  # Convert X to NXfield if it is not already
@@ -600,6 +608,175 @@ def plot_slice(data, X=None, Y=None, sum_axis=None, transpose=False, vmin=None,
600
608
  # Return the quadmesh object
601
609
  return p
602
610
 
611
+ def animate_slice_temp(temp_dependence, slice_obj, ax=None, interval=500, save_gif=False, filename='animation',
612
+ title=True, title_fmt='d', plot_slice_kwargs=None, ax_kwargs=None):
613
+ """
614
+ Animate 2D slices from a temperature-dependent dataset.
615
+
616
+ Creates a matplotlib animation by extracting 2D slices from each dataset
617
+ in a TempDependence object and animating them in sequence by temperature.
618
+ Optionally displays the animation inline and/or saves it as a GIF.
619
+
620
+ Parameters
621
+ ----------
622
+ temp_dependence : nxs_analysis_tools.chess.TempDependence
623
+ Object holding datasets at various temperatures.
624
+ slice_obj : list of slice or None
625
+ Slice object to apply to each dataset; None entries are treated as ':'.
626
+ ax : matplotlib.axes.Axes, optional
627
+ The axes object to plot on. If None, a new figure and axes will be created.
628
+ interval : int, optional
629
+ Delay between frames in milliseconds. Default is 500.
630
+ save_gif : bool, optional
631
+ If True, saves the animation to a .gif file. Default is False.
632
+ filename : str, optional
633
+ Filename (without extension) for saved .gif. Default is 'animation'.
634
+ title : bool, optional
635
+ If True, displays the temperature in the title of each frame. Default is True.
636
+ title_fmt : str, optional
637
+ Format string for temperature values (e.g., '.2f' for 2 decimals). Default is 'd' (integer).
638
+ plot_slice_kwargs : dict, optional
639
+ Additional keyword arguments passed to `plot_slice`.
640
+ ax_kwargs : dict, optional
641
+ Keyword arguments passed to `ax.set`.
642
+
643
+ Returns
644
+ -------
645
+ ani : matplotlib.animation.FuncAnimation
646
+ The resulting animation object.
647
+ """
648
+ if ax is None:
649
+ fig,ax = plt.subplots() # Generate a new figure and axis
650
+ else:
651
+ fig = ax.figure # Get the figure from the provided axis
652
+
653
+
654
+ if plot_slice_kwargs is None:
655
+ plot_slice_kwargs = {}
656
+ if ax_kwargs is None:
657
+ ax_kwargs = {}
658
+
659
+ # Normalize the slice object
660
+ normalized_slice = [slice(None) if s is None else s for s in slice_obj]
661
+
662
+ # Warn if colorbar is requested
663
+ if plot_slice_kwargs.get('cbar', False):
664
+ warnings.warn("Colorbar is not supported in animation and will be ignored.", UserWarning)
665
+ plot_slice_kwargs['cbar'] = False
666
+ elif 'cbar' not in plot_slice_kwargs.keys():
667
+ plot_slice_kwargs['cbar'] = False
668
+
669
+ def update(temp):
670
+ ax.clear()
671
+ dataset = temp_dependence.datasets[temp]
672
+ plot_slice(dataset[tuple(normalized_slice)], ax=ax, **plot_slice_kwargs)
673
+ ax.set(**ax_kwargs)
674
+
675
+ if title:
676
+ try:
677
+ formatted_temp = f"{int(temp):{title_fmt}}"
678
+ except ValueError:
679
+ raise ValueError(f"Invalid title_fmt '{title_fmt}' for temperature value '{temp}'")
680
+ ax.set(title=f'$T$={formatted_temp}')
681
+
682
+ ani = animation.FuncAnimation(fig, update,
683
+ frames=temp_dependence.temperatures,
684
+ interval=interval, repeat=False)
685
+
686
+ display(HTML(ani.to_jshtml()))
687
+
688
+ if save_gif:
689
+ gif_file = f'{filename}.gif'
690
+ writer = animation.PillowWriter(fps=1000 / interval)
691
+ ani.save(gif_file, writer=writer)
692
+ with open(gif_file, 'rb') as f:
693
+ display(Image(f.read(), format='gif'))
694
+
695
+ return ani
696
+
697
+ def animate_slice_axis(data, axis, axis_values, ax=None, interval=500, save_gif=False, filename='animation', title=True, title_fmt='.2f', plot_slice_kwargs={}, ax_kwargs={}):
698
+ """
699
+ Animate 2D slices of a 3D dataset along a given axis.
700
+
701
+ Creates a matplotlib animation by sweeping through 2D slices of a 3D
702
+ dataset along the specified axis. Optionally displays the animation
703
+ inline (e.g., in Jupyter) and/or saves it as a GIF.
704
+
705
+ Parameters
706
+ ----------
707
+ data : nexusformat.nexus.NXdata
708
+ The 3D dataset to visualize.
709
+ axis : int
710
+ The axis along which to animate (must be 0, 1, or 2).
711
+ axis_values : iterable
712
+ The values along the animation axis to use as animation frames.
713
+ ax : matplotlib.axes.Axes, optional
714
+ The axes object to plot on. If None, a new figure and axes will be created.
715
+ interval : int, optional
716
+ Delay between frames in milliseconds. Default is 500.
717
+ save_gif : bool, optional
718
+ If True, saves the animation as a .gif file. Default is False.
719
+ filename : str, optional
720
+ Filename (without extension) to use for the saved .gif. Default is 'animation'.
721
+ title : bool, optional
722
+ If True, displays the axis value as a title for each frame. Default is True.
723
+ title_fmt : str, optional
724
+ Format string for axis value in the title (e.g., '.2f' for 2 decimals). Default is '.2f'.
725
+ plot_slice_kwargs : dict, optional
726
+ Additional keyword arguments passed to `plot_slice`.
727
+ ax_kwargs : dict, optional
728
+ Keyword arguments passed to `ax.set` to update axis settings.
729
+
730
+ Returns
731
+ -------
732
+ ani : matplotlib.animation.FuncAnimation
733
+ The animation object.
734
+ """
735
+ if ax is None:
736
+ fig,ax = plt.subplots() # Generate a new figure and axis
737
+ else:
738
+ fig = ax.figure # Get the figure from the provided axis
739
+
740
+ if axis not in [0, 1, 2]:
741
+ raise ValueError("axis must be either 0, 1, or 2.")
742
+
743
+ if plot_slice_kwargs.get('cbar', False):
744
+ warnings.warn("Colorbar is not supported in animation and will be ignored.", UserWarning)
745
+ plot_slice_kwargs['cbar'] = False
746
+ elif 'cbar' not in plot_slice_kwargs.keys():
747
+ plot_slice_kwargs['cbar'] = False
748
+
749
+
750
+
751
+ def update(parameter):
752
+ ax.clear()
753
+
754
+ # Construct slicing object for the selected axis
755
+ slice_obj = [slice(None)] * 3
756
+ slice_obj[axis] = parameter
757
+
758
+ # Plot the 2D slice
759
+ plot_slice(data[tuple(slice_obj)], ax=ax, **plot_slice_kwargs)
760
+ ax.set(**ax_kwargs)
761
+
762
+ if title:
763
+ axis_label = data.axes[axis]
764
+ ax.set(title=f'${axis_label}$={parameter:{title_fmt}}')
765
+
766
+ ani = animation.FuncAnimation(fig, update, frames=axis_values, interval=interval, repeat=False)
767
+
768
+ display(HTML(ani.to_jshtml()))
769
+
770
+ if save_gif:
771
+ gif_file = f'{filename}.gif'
772
+ writergif = animation.PillowWriter(fps=1000/interval)
773
+ ani.save(gif_file, writer=writergif)
774
+ display(HTML(ani.to_jshtml()))
775
+ with open(gif_file, 'rb') as file:
776
+ display(Image(file.read(), format='gif'))
777
+
778
+ return ani
779
+
603
780
 
604
781
  class Scissors:
605
782
  """
@@ -3,8 +3,9 @@ Module for fitting of linecuts using the lmfit package.
3
3
  """
4
4
 
5
5
  import operator
6
- from lmfit.model import Model
7
- from lmfit.model import CompositeModel
6
+ from lmfit import Parameters
7
+ from lmfit.model import Model, CompositeModel
8
+ from lmfit.models import PseudoVoigtModel, LinearModel
8
9
  import matplotlib.pyplot as plt
9
10
  import numpy as np
10
11
 
@@ -66,6 +67,8 @@ class LinecutModel:
66
67
  Fit the model to the data.
67
68
  plot_fit(self, numpoints=None, fit_report=True, **kwargs)
68
69
  Plot the fitted model.
70
+ fit_peak_simple():
71
+ Perform a basic fit using a pseudo-Voigt peak shape, linear background, and no constraints.
69
72
  print_fit_report(self)
70
73
  Print the fit report.
71
74
  """
@@ -110,15 +113,25 @@ class LinecutModel:
110
113
 
111
114
  Parameters
112
115
  ----------
113
- model_components : Model or list of Models
114
- The model component(s) to be used for fitting,
115
- which will be combined into a CompositeModel.
116
+ model_components : Model, CompositeModel, or iterable of Model
117
+ The model component(s) to be used for fitting.
116
118
  """
117
119
 
118
120
  # If the model only has one component, then use it as the model
119
121
  if isinstance(model_components, Model):
120
122
  self.model = model_components
121
- # Else, combine the components into a composite model and use that as the
123
+ self.params = self.model.make_params()
124
+
125
+ # If the model is a composite model, then use it as the model
126
+ elif isinstance(model_components, CompositeModel):
127
+ self.model = model_components
128
+ self.model_components = self.model.components
129
+ # Make params for each component of the model
130
+ self.params = Parameters()
131
+ for component in self.model.components:
132
+ self.params.update(component.make_params())
133
+
134
+ # Else, combine the components into a composite model and use that as the model
122
135
  else:
123
136
  self.model_components = model_components
124
137
  self.model = model_components[0]
@@ -127,9 +140,15 @@ class LinecutModel:
127
140
  for component in model_components[1:]:
128
141
  self.model = CompositeModel(self.model, component, operator.add)
129
142
 
143
+ # Make params for each component of the model
144
+ self.params = Parameters()
145
+ for component in self.model.components:
146
+ self.params.update(component.make_params())
147
+
130
148
  def set_param_hint(self, *args, **kwargs):
131
149
  """
132
- Set parameter hints for the model.
150
+ Set parameter hints for the model. These are implemented when the .make_params() method
151
+ is called.
133
152
 
134
153
  Parameters
135
154
  ----------
@@ -159,10 +178,22 @@ class LinecutModel:
159
178
 
160
179
  def guess(self):
161
180
  """
162
- Perform initial guesses for each model component.
181
+ Perform initial guesses for each model component and update params. This overwrites any
182
+ prior initial values and constraints.
183
+
184
+ Returns
185
+ -------
186
+ components_params : list
187
+ A list containing params objects for each component of the model.
163
188
  """
164
- for model_component in list(self.model_components):
189
+
190
+ components_params = []
191
+
192
+ for model_component in self.model.components:
165
193
  self.params.update(model_component.guess(self.y, x=self.x))
194
+ components_params.append(model_component.guess(self.y, x=self.x))
195
+
196
+ return components_params
166
197
 
167
198
  def print_initial_params(self):
168
199
  """
@@ -251,6 +282,17 @@ class LinecutModel:
251
282
  if fit_report:
252
283
  print(self.modelresult.fit_report())
253
284
  return ax
285
+
286
+ def fit_peak_simple(self):
287
+ """
288
+ Fit all linecuts in the temperature series using a pseudo-Voigt peak shape and linear
289
+ background, with no constraints.
290
+ """
291
+ self.set_model_components([PseudoVoigtModel(prefix='peak'),
292
+ LinearModel(prefix='background')])
293
+ self.make_params()
294
+ self.guess()
295
+ self.fit()
254
296
 
255
297
  def print_fit_report(self):
256
298
  """