nxs-analysis-tools 0.1.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,866 @@
1
+ """
2
+ This module provides classes and functions for analyzing scattering datasets collected at CHESS
3
+ (ID4B) with temperature dependence. It includes functions for loading temperature series and
4
+ performing operations on all datasets in the series at once (e.g., cutting, fitting).
5
+ """
6
+ import os
7
+ import re
8
+
9
+ import matplotlib.pyplot as plt
10
+ import matplotlib as mpl
11
+ import pandas as pd
12
+ import numpy as np
13
+ from IPython.display import display, Markdown
14
+ from nxs_analysis_tools import load_data, Scissors
15
+ from nxs_analysis_tools.fitting import LinecutModel
16
+ from nxs_analysis_tools.datareduction import load_transform, reciprocal_lattice_params
17
+ from lmfit.models import PseudoVoigtModel, LinearModel
18
+
19
+
20
+ class TempDependence:
21
+ """
22
+ A class for analyzing temperature-dependent scattering datasets collected at CHESS (ID4B).
23
+
24
+ The `TempDependence` class facilitates the loading, processing, and analysis of scattering
25
+ data across different temperatures. It includes methods for handling datasets, setting
26
+ lattice parameters, performing linecuts, modeling the data, and visualizing the results.
27
+
28
+ Attributes
29
+ ----------
30
+ sample_directory : str
31
+ Path to the directory containing the datasets.
32
+ xlabel : str
33
+ Label for the x-axis of plots, determined by the axis of the linecuts.
34
+ datasets : dict
35
+ Dictionary storing datasets keyed by temperature.
36
+ temperatures : list of str
37
+ List of temperatures for which data is available.
38
+ scissors : dict
39
+ Dictionary of Scissors objects, one for each temperature, used for data manipulation and
40
+ linecut operations.
41
+ linecuts : dict
42
+ Dictionary storing the linecut data for each temperature.
43
+ linecutmodels : dict
44
+ Dictionary of LinecutModel objects, one for each temperature, used for fitting the linecuts.
45
+ a, b, c, al, be, ga : float or None
46
+ Lattice parameters (a, b, c, alpha, beta, gamma) of the crystal.
47
+ a_star, b_star, c_star, al_star, be_star, ga_star : float or None
48
+ Reciprocal lattice parameters (a*, b*, c*, alpha*, beta*, gamma*).
49
+
50
+ Methods
51
+ -------
52
+ set_temperatures(temperatures):
53
+ Set the list of temperatures for the datasets.
54
+ find_temperatures():
55
+ Set the list of temperatures by automatically scanning the sample directory.
56
+ set_sample_directory(path):
57
+ Set the directory path where the datasets are located.
58
+ initialize():
59
+ Initialize Scissors and LinecutModel objects for each temperature.
60
+ set_data(temperature, data):
61
+ Set the dataset for a specific temperature.
62
+ load_transforms(temperatures_list=None, exclude_temperatures=None, print_tree=True):
63
+ Load transform datasets (from nxrefine) based on temperature.
64
+ load_datasets(file_ending='hkli.nxs', temperatures_list=None, exclude_temperatures=None,
65
+ print_tree=True):
66
+ Load datasets (legacy CHESS format) from the specified folder.
67
+ get_sample_directory():
68
+ Get the folder path where the datasets are located.
69
+ clear_datasets():
70
+ Clear the datasets stored in the TempDependence instance.
71
+ set_Lattice_params(lattice_params):
72
+ Set lattice parameters and calculate reciprocal lattice parameters.
73
+ set_window(window, verbose=False):
74
+ Set the extents of the integration window for each temperature.
75
+ set_center(center):
76
+ Set the central coordinate for the linecut for each temperature.
77
+ cut_data(center=None, window=None, axis=None, verbose=False):
78
+ Perform data cutting for each temperature dataset.
79
+ plot_linecuts(vertical_offset=0, **kwargs):
80
+ Plot the linecuts obtained from data cutting.
81
+ plot_linecuts_heatmap(ax=None, **kwargs):
82
+ Plot a heatmap of the linecuts obtained from data cutting.
83
+ highlight_integration_window(temperature=None, **kwargs):
84
+ Display the integration window plot for a specific temperature.
85
+ plot_integration_window(temperature=None, **kwargs):
86
+ Plot the integration window cross-sections for a specific temperature.
87
+ set_model_components(model_components):
88
+ Set the model components for all line cut models.
89
+ set_param_hint(*args, **kwargs):
90
+ Set parameter hints for all line cut models.
91
+ make_params():
92
+ Create parameters for all line cut models.
93
+ guess():
94
+ Make initial parameter guesses for all line cut models.
95
+ print_initial_params():
96
+ Print the initial parameter values for all line cut models.
97
+ plot_initial_guess():
98
+ Plot the initial guess for all line cut models.
99
+ fit(verbose=False):
100
+ Fit the line cut models for each temperature.
101
+ plot_fit(mdheadings=False, **kwargs):
102
+ Plot the fit results for each temperature.
103
+ overlay_fits(numpoints=None, vertical_offset=0, cmap='viridis', ax=ax,
104
+ data_kwargs=None, fit_kwargs=None):
105
+ Plot raw data and fitted models for each temperature.
106
+ fit_peak_simple():
107
+ Perform a basic fit using a pseudo-Voigt peak shape, linear background, and no constraints.
108
+ plot_order_parameter(ax, param_name='peakheight', **kwargs):
109
+ Plot the temperature dependence of the peakheight parameter.
110
+ print_fit_report():
111
+ Print the fit report for each temperature.
112
+ """
113
+
114
+ def __init__(self, sample_directory=None):
115
+ """
116
+ Initialize the TempDependence class.
117
+
118
+ Parameters
119
+ ----------
120
+ sample_directory : str, optional
121
+ Path to the directory containing the temperature folders.
122
+ If None, no directory is set initially.
123
+ """
124
+
125
+ if sample_directory is None:
126
+ self.sample_directory = None
127
+ else:
128
+ self.set_sample_directory(sample_directory)
129
+
130
+ self.xlabel = ''
131
+ self.datasets = {}
132
+ self.temperatures = []
133
+ self.scissors = {}
134
+ self.linecuts = {}
135
+ self.linecutmodels = {}
136
+ self.a, self.b, self.c, self.al, self.be, self.ga, \
137
+ self.a_star, self.b_star, self.c_star, self.al_star, self.be_star, self.ga_star \
138
+ = [None] * 12
139
+
140
+ def set_temperatures(self, temperatures):
141
+ """
142
+ Set the list of temperatures for the datasets.
143
+
144
+ Parameters
145
+ ----------
146
+ temperatures : list
147
+ List of temperatures to set.
148
+ """
149
+ self.temperatures = temperatures
150
+
151
+ def find_temperatures(self):
152
+ """
153
+ Set the list of temperatures by automatically scanning the sample directory for .nxs files from nxrefine.
154
+ """
155
+
156
+ # Assert that self.sample_directory must exist
157
+ if self.sample_directory is None:
158
+ raise ValueError("Sample directory is not set. Use set_sample_directory(path) first.")
159
+
160
+ # Clear existing temperatures
161
+ self.temperatures = []
162
+
163
+ # Search for nxrefine .nxs files
164
+ for item in os.listdir(self.sample_directory):
165
+ pattern = r'_(\d+)\.nxs'
166
+ match = re.search(pattern, item)
167
+ if match:
168
+ # Identify temperature
169
+ temperature = match.group(1)
170
+ self.temperatures.append(temperature)
171
+ # Convert all temperatures to int temporarily to sort temperatures list
172
+ self.temperatures = [int(t) for t in self.temperatures]
173
+ self.temperatures.sort()
174
+ self.temperatures = [str(t) for t in self.temperatures]
175
+
176
+ def set_sample_directory(self, path):
177
+ """
178
+ Set the directory path where the datasets are located.
179
+
180
+ Parameters
181
+ ----------
182
+ path : str
183
+ Path to the sample directory.
184
+ """
185
+ self.sample_directory = os.path.normpath(path)
186
+
187
+ def initialize(self):
188
+ """
189
+ Initialize Scissors and LinecutModel objects for each temperature.
190
+ """
191
+ for temperature in self.temperatures:
192
+ self.scissors[temperature] = Scissors()
193
+ if temperature in self.datasets.keys():
194
+ self.scissors[temperature].set_data(self.datasets[temperature])
195
+ self.linecutmodels[temperature] = LinecutModel()
196
+
197
+ def set_data(self, temperature, data):
198
+ """
199
+ Set the dataset for a specific temperature.
200
+
201
+ Parameters
202
+ ----------
203
+ temperature : str
204
+ Temperature for which to set the data.
205
+ data : object
206
+ The dataset to be set.
207
+ """
208
+ self.datasets[temperature] = data
209
+
210
+ def load_transforms(self, temperatures_list=None, exclude_temperatures=None, print_tree=True, use_nxlink=False):
211
+ """
212
+ Load transform datasets (from nxrefine) based on temperature.
213
+
214
+ Parameters
215
+ ----------
216
+ temperatures_list : list of int or None, optional
217
+ List of temperatures to load. If None, all available temperatures are loaded.
218
+
219
+ exclude_temperatures : int, str, optional
220
+ Temperatures to skip. Applied after filtering with `temperatures_list`, if provided.
221
+
222
+ print_tree : bool, optional
223
+ Whether to print the data tree upon loading. Default True.
224
+
225
+ use_nxlink : bool, optional
226
+ If True, maintains the NXlink defined in the data file, which references
227
+ the raw data in the transform.nxs file. This saves memory when working with
228
+ many datasets. In this case, the axes are in reverse order. Default is False.
229
+ """
230
+ # Convert all temperatures to strings
231
+ if temperatures_list:
232
+ temperatures_list = [str(t) for t in temperatures_list]
233
+ if exclude_temperatures:
234
+ if isinstance(exclude_temperatures, str):
235
+ exclude_temperatures = [exclude_temperatures]
236
+ exclude_temperatures = [str(t) for t in list(exclude_temperatures)]
237
+
238
+ # Clear existing temperatures before loading files
239
+ self.temperatures = []
240
+
241
+ # Identify files to load
242
+ items_to_load = []
243
+ # Search for nxrefine .nxs files
244
+ for item in os.listdir(self.sample_directory):
245
+ pattern = r'_(\d+)\.nxs'
246
+ match = re.search(pattern, item)
247
+ if match:
248
+ # Identify temperature
249
+ temperature = match.group(1)
250
+ # print(f'Temperature = {temperature}')
251
+ if temperatures_list is not None:
252
+ incl_temp = temperature in temperatures_list
253
+ else:
254
+ incl_temp = True
255
+ if exclude_temperatures is not None:
256
+ not_excl_temp = temperature not in exclude_temperatures
257
+ else:
258
+ not_excl_temp = True
259
+ if incl_temp and not_excl_temp:
260
+ # Prepare file to be loaded
261
+ self.temperatures.append(temperature)
262
+ items_to_load.append(item)
263
+ # print(f'Preparing to load {temperature} K data: {item}')
264
+
265
+ # Convert all temperatures to int temporarily to sort temperatures list before loading
266
+ self.temperatures = [int(t) for t in self.temperatures]
267
+
268
+ loading_template = pd.DataFrame({'temperature': self.temperatures,
269
+ 'filename': items_to_load})
270
+ loading_template = loading_template.sort_values(by='temperature')
271
+ self.temperatures = loading_template['temperature']
272
+ self.temperatures = [str(t) for t in self.temperatures]
273
+ items_to_load = loading_template['filename'].to_list()
274
+
275
+ for i, item in enumerate(items_to_load):
276
+ path = os.path.join(self.sample_directory, item)
277
+
278
+ # Ensure path is a string before using it
279
+ path = str(path)
280
+
281
+ # Save dataset
282
+ try:
283
+ self.datasets[self.temperatures[i]] = load_transform(path, print_tree=print_tree, use_nxlink=use_nxlink)
284
+ except Exception as e:
285
+ # Report temperature that was unable to load, then raise exception.
286
+ temp_failed = self.temperatures[i]
287
+ print(f"Failed to load data for temperature {temp_failed} K from file {item}."
288
+ f" Error: {e}")
289
+ raise # Re-raise the exception
290
+
291
+ self.initialize()
292
+
293
+ def load_datasets(self, file_ending='hkli.nxs', temperatures_list=None, exclude_temperatures=None, print_tree=True):
294
+ """
295
+ Load datasets (CHESS format) from the specified folder.
296
+
297
+ Parameters
298
+ ----------
299
+ file_ending : str, optional
300
+ File extension of datasets to load. Default is 'hkli.nxs'.
301
+ temperatures_list : list of int or str, optional
302
+ Specific temperatures to load. If None, all temperatures are loaded.
303
+ exclude_temperatures : list of int or str, optional
304
+ Temperatures to skip. Applied after filtering with `temperatures_list`, if provided.
305
+ print_tree : bool, optional
306
+ If True, prints the NeXus tree structure for each file. Default is True.
307
+ """
308
+
309
+ if temperatures_list is not None:
310
+ self.temperatures = [str(t) for t in temperatures_list]
311
+ else:
312
+ self.temperatures = [] # Empty list to store temperature folder names
313
+ for item in os.listdir(self.sample_directory):
314
+ try:
315
+ self.temperatures.append(int(item)) # If folder name can be int, add it
316
+ except ValueError:
317
+ pass # Otherwise don't add it
318
+ self.temperatures.sort() # Sort from low to high T
319
+ self.temperatures = [str(i) for i in self.temperatures] # Convert to strings
320
+
321
+ if exclude_temperatures is not None:
322
+ [self.temperatures.remove(str(t)) for t in exclude_temperatures]
323
+
324
+ # Load .nxs files
325
+ for T in self.temperatures:
326
+ for file in os.listdir(os.path.join(self.sample_directory, T)):
327
+ if file.endswith(file_ending):
328
+ filepath = os.path.join(self.sample_directory, T, file)
329
+
330
+ # Load dataset at each temperature
331
+ self.datasets[T] = load_data(filepath, print_tree)
332
+
333
+ self.initialize()
334
+
335
+ def get_sample_directory(self):
336
+ """
337
+ Get the folder path where the datasets are located.
338
+
339
+ Returns
340
+ -------
341
+ str
342
+ The folder path.
343
+ """
344
+ return self.sample_directory
345
+
346
+ def clear_datasets(self):
347
+ """
348
+ Clear the datasets stored in the TempDependence instance.
349
+ """
350
+ self.datasets = {}
351
+
352
+ def set_lattice_params(self, lattice_params):
353
+ """
354
+ Set lattice parameters and calculate reciprocal lattice parameters.
355
+
356
+ Parameters
357
+ ----------
358
+ lattice_params : tuple
359
+ Tuple containing lattice parameters (a, b, c, al, be, ga).
360
+ """
361
+ self.a, self.b, self.c, self.al, self.be, self.ga = lattice_params
362
+ self.a_star, self.b_star, self.c_star, \
363
+ self.al_star, self.be_star, self.ga_star = reciprocal_lattice_params(lattice_params)
364
+
365
+ def set_window(self, window, verbose=False):
366
+ """
367
+ Set the extents of the integration window for each temperature.
368
+
369
+ Parameters
370
+ ----------
371
+ window : tuple
372
+ Extents of the window for integration along each axis.
373
+ verbose : bool, optional
374
+ Enables printout of linecut axis and integrated axes. Default is False.
375
+ """
376
+ for T in self.temperatures:
377
+ if verbose:
378
+ print("----------------------------------")
379
+ print("T = " + T + " K")
380
+ self.scissors[T].set_window(window, verbose)
381
+
382
+ def set_center(self, center):
383
+ """
384
+ Set the central coordinate for the linecut for each temperature.
385
+
386
+ Parameters
387
+ ----------
388
+ center : tuple
389
+ Central coordinate around which to perform the linecut.
390
+ """
391
+ for T in self.temperatures:
392
+ self.scissors[T].set_center(center)
393
+
394
+ def cut_data(self, center=None, window=None, axis=None, verbose=False):
395
+ """
396
+ Perform data cutting for each temperature dataset.
397
+
398
+ Parameters
399
+ ----------
400
+ center : tuple, optional
401
+ The center point for cutting the data.
402
+ Defaults to the first temperature's center if None.
403
+ window : tuple, optional
404
+ The window size for cutting the data.
405
+ Defaults to the first temperature's window if None.
406
+ axis : int or None, optional
407
+ The axis along which to perform the cutting.
408
+ Defaults to the longest axis in `window` if None.
409
+ verbose : bool, optional
410
+ Enables printout of linecut progress. Default is False.
411
+
412
+ Returns
413
+ -------
414
+ dict
415
+ A dictionary of linecuts obtained from the cutting operation.
416
+ """
417
+
418
+ for T in self.temperatures:
419
+ if verbose:
420
+ print("-------------------------------")
421
+ print("Cutting T = " + T + " K data...")
422
+ self.scissors[T].set_center(center)
423
+ self.scissors[T].set_window(window)
424
+ self.scissors[T].cut_data(axis=axis, verbose=verbose)
425
+ self.linecuts[T] = self.scissors[T].linecut
426
+ self.linecutmodels[T].set_data(self.linecuts[T])
427
+
428
+ xlabel_components = [self.linecuts[self.temperatures[0]].axes
429
+ if i == self.scissors[self.temperatures[0]].axis
430
+ else str(c) for i, c in
431
+ enumerate(self.scissors[self.temperatures[0]].center)]
432
+ self.xlabel = ' '.join(xlabel_components)
433
+
434
+ return self.linecuts
435
+
436
+ def plot_linecuts(self, ax=None, vertical_offset=0, **kwargs):
437
+ """
438
+ Plot the linecuts obtained from data cutting.
439
+
440
+ Parameters
441
+ ----------
442
+ ax : matplotlib.axes.Axes, optional
443
+ The matplotlib Axes object on which to plot. If None, a new figure
444
+ and axes are created. Default None.
445
+ vertical_offset : float, optional
446
+ The vertical offset between linecuts on the plot. The default is 0.
447
+ **kwargs
448
+ Additional keyword arguments passed to the matplotlib plot function.
449
+ """
450
+ if ax is None:
451
+ fig, ax = plt.subplots()
452
+
453
+ # Get the Viridis colormap
454
+ cmap = mpl.colormaps.get_cmap('viridis')
455
+
456
+ # Reverse zorder
457
+ zorder = 0
458
+
459
+ for i, linecut in enumerate(self.linecuts.values()):
460
+
461
+ x_data = linecut[linecut.axes].nxdata
462
+ y_data = linecut[linecut.signal].nxdata + i * vertical_offset
463
+ p = ax.plot(x_data, y_data, color=cmap(i / len(self.linecuts)), label=self.temperatures[i],
464
+ zorder=zorder, **kwargs)
465
+ zorder -= 1
466
+
467
+ ax.set(xlabel=self.xlabel,
468
+ ylabel=self.linecuts[self.temperatures[0]].signal)
469
+
470
+ # Get the current legend handles and labels
471
+ handles, labels = plt.gca().get_legend_handles_labels()
472
+
473
+ # Reverse the order of handles and labels
474
+ handles = handles[::-1]
475
+ labels = labels[::-1]
476
+
477
+ # Create a new legend with reversed order
478
+ plt.legend(handles, labels)
479
+
480
+ def plot_linecuts_heatmap(self, ax=None, **kwargs):
481
+ """
482
+ Plot the linecuts obtained from data cutting.
483
+
484
+ Parameters
485
+ ----------
486
+ ax : :class:`matplotlib.axes.Axes`, optional
487
+ The axes on which to plot the heatmap. If None, a new figure and axes
488
+ are created. The default is None.
489
+ **kwargs
490
+ Additional keyword arguments to be passed to the `pcolormesh` function.
491
+
492
+ Returns
493
+ -------
494
+ QuadMesh
495
+ The plotted heatmap object.
496
+ """
497
+
498
+ # Retrieve linecut data for the first temperature and extract x-axis data
499
+ cut = self.linecuts[self.temperatures[0]]
500
+ x = cut[cut.axes].nxdata
501
+
502
+ # Convert the list of temperatures to a NumPy array for the y-axis
503
+ y = np.array([int(t) for t in self.temperatures])
504
+
505
+ # Collect counts from each temperature and ensure they are numpy arrays
506
+ v = [self.linecuts[T].nxsignal.nxdata for T in self.temperatures]
507
+
508
+ # Convert list of arrays to a 2D array for the heatmap
509
+ v_2d = np.array(v)
510
+
511
+ # Create the grid for the heatmap
512
+ X, Y = np.meshgrid(x, y)
513
+
514
+ # Plot using pcolormesh
515
+ if ax is None:
516
+ _, ax = plt.subplots()
517
+ p = ax.pcolormesh(X, Y, v_2d, **kwargs)
518
+ plt.colorbar(p, label='counts')
519
+ ax.set(xlabel=self.xlabel, ylabel=r'$T$ (K)')
520
+
521
+ return p
522
+
523
+ def highlight_integration_window(self, temperature=None, **kwargs):
524
+ """
525
+ Displays the integration window plot for a specific temperature,
526
+ or for the first temperature if none is provided.
527
+
528
+ Parameters
529
+ ----------
530
+ temperature : str, optional
531
+ The temperature at which to display the integration window plot. If provided, the plot
532
+ will be generated using the dataset corresponding to the specified temperature. If not
533
+ provided, the integration window plots will be generated for the first temperature.
534
+ **kwargs : keyword arguments, optional
535
+ Additional keyword arguments to customize the plot.
536
+ """
537
+
538
+ if temperature is not None:
539
+ p = self.scissors[
540
+ self.temperatures[0]].highlight_integration_window(
541
+ data=self.datasets[temperature], **kwargs
542
+ )
543
+ else:
544
+ p = self.scissors[self.temperatures[0]].highlight_integration_window(
545
+ data=self.datasets[self.temperatures[0]], **kwargs
546
+ )
547
+
548
+ return p
549
+
550
+ def plot_integration_window(self, temperature=None, **kwargs):
551
+ """
552
+ Plots the three principal cross-sections of the integration volume on
553
+ a single figure for a specific temperature, or for the first temperature
554
+ if none is provided.
555
+
556
+ Parameters
557
+ ----------
558
+ temperature : str, optional
559
+ The temperature at which to plot the integration volume. If provided,
560
+ the plot will be generated using the dataset corresponding to the
561
+ specified temperature. If not provided, the integration window plots
562
+ will be generated for the first temperature.
563
+
564
+ **kwargs : keyword arguments, optional
565
+ Additional keyword arguments to customize the plot.
566
+ """
567
+
568
+ if temperature is not None:
569
+ p = self.scissors[self.temperatures[0]].plot_integration_window(**kwargs)
570
+ else:
571
+ p = self.scissors[self.temperatures[0]].plot_integration_window(**kwargs)
572
+
573
+ return p
574
+
575
+ def set_model_components(self, model_components):
576
+ """
577
+ Set the model components for all line cut models.
578
+
579
+ This method sets the same model components for all line cut models in the
580
+ analysis. It iterates over each line cut model and calls their respective
581
+ `set_model_components` method with the provided `model_components`.
582
+
583
+ Parameters
584
+ ----------
585
+ model_components : Model, CompositeModel, or iterable of Model
586
+ The model components to set for all line cut models.
587
+
588
+ """
589
+ [linecutmodel.set_model_components(model_components) for
590
+ linecutmodel in self.linecutmodels.values()]
591
+
592
+ def set_param_hint(self, *args, **kwargs):
593
+ """
594
+ Set parameter hints for all line cut models.
595
+
596
+ This method sets the parameter hints for all line cut models in the analysis.
597
+ It iterates over each line cut model and calls their respective `set_param_hint` method
598
+ with the provided arguments and keyword arguments. These are implemented when the
599
+ .make_params() method is called.
600
+
601
+ Parameters
602
+ ----------
603
+ *args
604
+ Variable length argument list.
605
+ **kwargs
606
+ Arbitrary keyword arguments.
607
+
608
+ """
609
+ [linecutmodel.set_param_hint(*args, **kwargs)
610
+ for linecutmodel in self.linecutmodels.values()]
611
+
612
+ def params_set(self, name, **kwargs):
613
+ """
614
+ Set constraints on a parameter for all line cut models.
615
+
616
+ This method updates the specified parameter across all models in
617
+ `self.linecutmodels` using the keyword arguments provided. These
618
+ keyword arguments are passed to the `set()` method of the parameter,
619
+ which comes from a `lmfit.Parameters` object.
620
+
621
+ Parameters
622
+ ----------
623
+ name : str
624
+ Name of the parameter to modify (must exist in each model).
625
+ **kwargs
626
+ Constraint arguments passed to `Parameter.set()`, such as `value`,
627
+ `min`, `max`, `vary`, etc.
628
+
629
+ Raises
630
+ ------
631
+ KeyError
632
+ If the parameter `name` does not exist in one of the models.
633
+
634
+ Example
635
+ -------
636
+ >>> sample.params_set('peakamplitude', value=5, min=0, vary=True)
637
+ """
638
+
639
+ for linecutmodel in self.linecutmodels.values():
640
+ linecutmodel.params[name].set(**kwargs)
641
+
642
+ def make_params(self):
643
+ """
644
+ Create and initialize the parameters for all models.
645
+
646
+ This method creates the parameters for all line cut models in the analysis.
647
+ It iterates over each line cut model and calls their respective `make_params` method.
648
+ """
649
+ [linecutmodel.make_params() for linecutmodel in self.linecutmodels.values()]
650
+
651
+ def guess(self):
652
+ """
653
+ Make initial parameter guesses for all line cut models. This overwrites any prior initial
654
+ values and constraints.
655
+
656
+ This method generates initial parameter guesses for all line cut models in the analysis.
657
+ It iterates over each line cut model and calls their respective `guess` method.
658
+
659
+ """
660
+ [linecutmodel.guess() for linecutmodel in self.linecutmodels.values()]
661
+
662
+ def print_initial_params(self):
663
+ """
664
+ Print the initial parameter values for all line cut models.
665
+
666
+ This method prints the initial parameter values for all line cut models
667
+ in the analysis. It iterates over each line cut model and calls their
668
+ respective `print_initial_params` method.
669
+
670
+ """
671
+ [linecutmodel.print_initial_params() for linecutmodel in self.linecutmodels.values()]
672
+
673
+ def plot_initial_guess(self):
674
+ """
675
+ Plot the initial guess for all line cut models.
676
+
677
+ This method plots the initial guess for all line cut models in the analysis.
678
+ It iterates over each line cut model and calls their respective `plot_initial_guess` method.
679
+
680
+ """
681
+ for T, linecutmodel in self.linecutmodels.items():
682
+ _, ax = plt.subplots()
683
+ ax.set(title=T + ' K')
684
+ linecutmodel.plot_initial_guess()
685
+
686
+ def fit(self, verbose=False):
687
+ """
688
+ Fit the line cut models.
689
+
690
+ This method fits the line cut models for each temperature in the analysis.
691
+ It iterates over each line cut model, performs the fit, and prints the fitting progress.
692
+
693
+ Parameters
694
+ ----------
695
+ verbose : bool, optional
696
+ Enables printout of fitting progress. Default False.
697
+
698
+ """
699
+ for T, linecutmodel in self.linecutmodels.items():
700
+ if verbose:
701
+ print(f"Fitting {T} K data...")
702
+ linecutmodel.fit()
703
+ if verbose:
704
+ print("Done.")
705
+ print("Fits completed.")
706
+
707
+ def plot_fit(self, mdheadings=False, **kwargs):
708
+ """
709
+ Plot the fit results.
710
+
711
+ This method plots the fit results for each temperature in the analysis.
712
+ It iterates over each line cut model, calls their respective `plot_fit` method,
713
+ and sets the xlabel, ylabel, and title for the plot.
714
+
715
+ """
716
+ for T, linecutmodel in self.linecutmodels.items():
717
+ # Create a markdown heading for the plot
718
+ if mdheadings:
719
+ display(Markdown(f"### {T} K Fit Results"))
720
+ # Plot fit
721
+ linecutmodel.plot_fit(xlabel=self.xlabel,
722
+ ylabel=self.datasets[self.temperatures[0]].signal,
723
+ title=f"{T} K",
724
+ **kwargs)
725
+
726
+ def overlay_fits(self, numpoints=None, vertical_offset=0, cmap='viridis', ax=None,
727
+ data_kwargs=None, fit_kwargs=None):
728
+ """
729
+ Plot raw data and fitted models for each temperature with optional vertical offsets.
730
+
731
+ Parameters:
732
+ -----------
733
+ numpoints : int or None, default=None
734
+ Number of points to evaluate for the fitted model curves.
735
+ If None, uses the number of raw data points for each linecut.
736
+ vertical_offset : float, default=0
737
+ Amount to vertically offset each linecut for clarity.
738
+ cmap : str, default='viridis'
739
+ Name of the matplotlib colormap used to distinguish different temperatures.
740
+ ax : :class:`matplotlib.axes.Axes` or None, default=None
741
+ Axis object to plot on. If None, a new figure and axis are created.
742
+ data_kwargs : dict
743
+ Keyword arguments to be passed to the data plot function.
744
+ fit_kwargs : dict
745
+ Keyword arguments to be passed to the fit plot function.
746
+
747
+
748
+ The function:
749
+ - Uses a colormap to assign unique colors to each temperature.
750
+ - Plots raw data alongside evaluated fit models for each linecut.
751
+ - Vertically offsets each trace by a constant value for visual separation.
752
+ - Displays a legend in reverse order to match top-to-bottom visual stacking.
753
+ - Automatically labels the x- and y-axes based on NeXus-style data metadata.
754
+ """
755
+
756
+ # Create a figure and axes if an axis is not already provided
757
+ _, ax = plt.subplots() if ax is None else (None, ax)
758
+
759
+ if data_kwargs is None:
760
+ data_kwargs = {}
761
+ if fit_kwargs is None:
762
+ fit_kwargs = {}
763
+
764
+ # Generate a color palette for the various temperatures
765
+ cmap = plt.get_cmap(cmap)
766
+ colors = [cmap(i / len(self.temperatures)) for i, _ in enumerate(self.temperatures)]
767
+
768
+ for i, lm in enumerate(self.linecutmodels.values()):
769
+ # Plot the raw data
770
+ ax.plot(lm.x, lm.y + vertical_offset * i, '.', c=colors[i], **data_kwargs)
771
+
772
+ # Evaluate the fit
773
+ numpoints = len(lm.x) if numpoints is None else numpoints
774
+ x_eval = np.linspace(lm.x.min(), lm.x.max(), numpoints)
775
+ y_eval = lm.modelresult.eval(x=x_eval)
776
+ ax.plot(x_eval, y_eval + vertical_offset * i, '-', c=colors[i], label=self.temperatures[i], **fit_kwargs)
777
+
778
+ # Reverse legend entries to match top-to-bottom stacking
779
+ handles, labels = ax.get_legend_handles_labels()
780
+ ax.legend(handles[::-1], labels[::-1])
781
+
782
+ # Add axis labels
783
+ ax.set(xlabel=lm.data.nxaxes[0].nxname, ylabel=lm.data.nxsignal.nxname)
784
+
785
+ def fit_peak_simple(self):
786
+ """
787
+ Fit all linecuts in the temperature series using a pseudo-Voigt peak shape and linear
788
+ background, with no constraints.
789
+ """
790
+
791
+ for T in self.temperatures:
792
+ linecutmodel = self.linecutmodels[T]
793
+ linecutmodel.set_model_components([PseudoVoigtModel(prefix='peak'),
794
+ LinearModel(prefix='background')])
795
+ linecutmodel.make_params()
796
+ linecutmodel.guess()
797
+ linecutmodel.params['peakamplitude'].set(min=0)
798
+ linecutmodel.fit()
799
+
800
+ def plot_order_parameter(self, param_name='peakheight', ax=None, **kwargs):
801
+ """
802
+ Plot the temperature dependence of the peak height (order parameter).
803
+
804
+ This method extracts the values of a chosen parameter from each temperature-dependent
805
+ line cut fit stored in `linecutmodels` and plots it as a function of temperature.
806
+
807
+ Parameters
808
+ ----------
809
+ ax : :class:`matplotlib.axes.Axes`, optional
810
+ Axis object to plot on. If None, a new figure and axis are created.
811
+ param_name : str, optional
812
+ The name of the lmfit parameter to extract. Default is 'peakheight'.
813
+ **kwargs
814
+ Keyword arguments to be passed to the plot function.
815
+
816
+
817
+ Returns
818
+ -------
819
+ Figure
820
+ Matplotlib Figure object containing the peak height vs. temperature plot.
821
+ Axes
822
+ Matplotlib Axes object associated with the figure.
823
+
824
+ Notes
825
+ -----
826
+ - Temperature values are converted to integers for plotting.
827
+ - Peak heights are extracted from the 'peakheight' parameter in the model results.
828
+ - The plot uses standard axes labels with temperature in Kelvin.
829
+ """
830
+
831
+ # Create an array of temperature values
832
+ temperatures = [int(T) for T in self.temperatures]
833
+
834
+ # Create an empty list for the peak heights
835
+ peakheights = []
836
+
837
+ # Extract the peakheight at every temperature
838
+ for T in self.temperatures:
839
+
840
+ # Verify that the fit has already been completed
841
+ if self.linecutmodels[T].modelresult is None:
842
+ raise AttributeError("Model result is empty. Have you fit the data to a model?")
843
+
844
+ peakheights.append(self.linecutmodels[T].modelresult.params[param_name].value)
845
+
846
+ # Plot the peakheights vs. temperature
847
+ if ax is None:
848
+ fig, ax = plt.subplots()
849
+ else:
850
+ fig = ax.figure
851
+ ax.plot(temperatures, peakheights, **kwargs)
852
+ ax.set(xlabel='$T$ (K)', ylabel=param_name)
853
+ return fig, ax
854
+
855
+ def print_fit_report(self):
856
+ """
857
+ Plot the fit results.
858
+
859
+ This method plots the fit results for each temperature in the analysis.
860
+ It iterates over each line cut model, calls their respective `plot_fit` method,
861
+ and sets the xlabel, ylabel, and title for the plot.
862
+
863
+ """
864
+ for T, linecutmodel in self.linecutmodels.items():
865
+ print(f"[[[{T} K Fit Report]]]")
866
+ linecutmodel.print_fit_report()