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.
- _meta/__init__.py +10 -0
- nxs_analysis_tools/__init__.py +15 -0
- nxs_analysis_tools/chess.py +866 -0
- nxs_analysis_tools/datareduction.py +1542 -0
- nxs_analysis_tools/datasets.py +137 -0
- nxs_analysis_tools/fitting.py +301 -0
- nxs_analysis_tools/lineartransformations.py +51 -0
- nxs_analysis_tools/pairdistribution.py +1758 -0
- nxs_analysis_tools-0.1.13.dist-info/METADATA +89 -0
- nxs_analysis_tools-0.1.13.dist-info/RECORD +13 -0
- nxs_analysis_tools-0.1.13.dist-info/WHEEL +5 -0
- nxs_analysis_tools-0.1.13.dist-info/licenses/LICENSE +21 -0
- nxs_analysis_tools-0.1.13.dist-info/top_level.txt +2 -0
|
@@ -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()
|