nxs-analysis-tools 0.0.34__py3-none-any.whl → 0.0.36__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nxs-analysis-tools might be problematic. Click here for more details.

_meta/__init__.py CHANGED
@@ -1,10 +1,10 @@
1
- '''nxs-analysis-tools package metadata.'''
2
-
3
- # keep consistent with pyproject.toml
4
- __project__ = 'nxs-analysis-tools'
5
- __author__ = 'Steven J. Gomez Alvarado'
6
- __email__ = 'stevenjgomez@ucsb.edu'
7
- __copyright__ = f"2023, {__author__}"
8
- __license__ = 'MIT'
9
- __version__= '0.0.34'
10
- __repo_url__ = 'https://github.com/stevenjgomez/nxs_analysis_tools'
1
+ '''nxs-analysis-tools package metadata.'''
2
+
3
+ # keep consistent with pyproject.toml
4
+ __project__ = 'nxs-analysis-tools'
5
+ __author__ = 'Steven J. Gomez Alvarado'
6
+ __email__ = 'stevenjgomez@ucsb.edu'
7
+ __copyright__ = f"2023, {__author__}"
8
+ __license__ = 'MIT'
9
+ __version__= '0.0.36'
10
+ __repo_url__ = 'https://github.com/stevenjgomez/nxs_analysis_tools'
@@ -1,12 +1,13 @@
1
- '''
2
- Reduce and transform nexus format (.nxs) scattering data.
3
- '''
4
-
5
- import numpy as np
6
- from _meta import __author__, __copyright__, __license__, __version__
7
- from .datareduction import load_data, load_transform, plot_slice, reciprocal_lattice_params, Scissors, rotate_data
8
- from .chess import TempDependence
9
-
10
- # What to import when running "from nxs_analysis_tools import *"
11
- __all__ = ['load_data', 'load_transform', 'plot_slice', 'Scissors', 'TempDependence',
12
- 'reciprocal_lattice_params', 'rotate_data']
1
+ '''
2
+ Reduce and transform nexus format (.nxs) scattering data.
3
+ '''
4
+
5
+ import numpy as np
6
+ from _meta import __author__, __copyright__, __license__, __version__
7
+ from .datareduction import load_data, load_transform, plot_slice,\
8
+ reciprocal_lattice_params, Scissors, rotate_data
9
+ from .chess import TempDependence
10
+
11
+ # What to import when running "from nxs_analysis_tools import *"
12
+ __all__ = ['load_data', 'load_transform', 'plot_slice', 'Scissors', 'TempDependence',
13
+ 'reciprocal_lattice_params', 'rotate_data']
@@ -4,11 +4,12 @@ This module provides classes and functions for analyzing scattering datasets col
4
4
  plotting linecuts.
5
5
  """
6
6
  import os
7
+ import re
8
+
7
9
  import matplotlib.pyplot as plt
8
10
  import matplotlib as mpl
9
11
  import numpy as np
10
12
  from IPython.display import display, Markdown
11
- from nexusformat.nexus import nxload
12
13
  from nxs_analysis_tools import load_data, Scissors
13
14
  from nxs_analysis_tools.fitting import LinecutModel
14
15
  from nxs_analysis_tools.datareduction import load_transform, reciprocal_lattice_params
@@ -16,14 +17,93 @@ from nxs_analysis_tools.datareduction import load_transform, reciprocal_lattice_
16
17
 
17
18
  class TempDependence:
18
19
  """
19
- Class for analyzing scattering datasets collected at CHESS (ID4B) with temperature dependence.
20
+ A class for analyzing temperature-dependent scattering datasets collected at CHESS (ID4B).
21
+
22
+ The `TempDependence` class facilitates the loading, processing, and analysis of scattering
23
+ data across different temperatures. It includes methods for handling datasets, setting
24
+ lattice parameters, performing linecuts, modeling the data, and visualizing the results.
25
+
26
+ Attributes
27
+ ----------
28
+ sample_directory : str
29
+ Path to the directory containing the datasets.
30
+ xlabel : str
31
+ Label for the x-axis of plots, determined by the axis of the linecuts.
32
+ datasets : dict
33
+ Dictionary storing datasets keyed by temperature.
34
+ temperatures : list of str
35
+ List of temperatures for which data is available.
36
+ scissors : dict
37
+ Dictionary of Scissors objects, one for each temperature, used for data manipulation and
38
+ linecut operations.
39
+ linecuts : dict
40
+ Dictionary storing the linecut data for each temperature.
41
+ linecutmodels : dict
42
+ Dictionary of LinecutModel objects, one for each temperature, used for fitting the linecuts.
43
+ a, b, c, al, be, ga : float or None
44
+ Lattice parameters (a, b, c, alpha, beta, gamma) of the crystal.
45
+ a_star, b_star, c_star, al_star, be_star, ga_star : float or None
46
+ Reciprocal lattice parameters (a*, b*, c*, alpha*, beta*, gamma*).
47
+
48
+ Methods
49
+ -------
50
+ set_temperatures(temperatures):
51
+ Set the list of temperatures for the datasets.
52
+ set_sample_directory(path):
53
+ Set the directory path where the datasets are located.
54
+ initialize():
55
+ Initialize Scissors and LinecutModel objects for each temperature.
56
+ set_data(temperature, data):
57
+ Set the dataset for a specific temperature.
58
+ load_transforms(temperatures_list=None):
59
+ Load transform datasets (from nxrefine) based on temperature.
60
+ load_datasets(file_ending='hkli.nxs', temperatures_list=None):
61
+ Load datasets (CHESS format) from the specified folder.
62
+ get_sample_directory():
63
+ Get the folder path where the datasets are located.
64
+ clear_datasets():
65
+ Clear the datasets stored in the TempDependence instance.
66
+ set_Lattice_params(lattice_params):
67
+ Set lattice parameters and calculate reciprocal lattice parameters.
68
+ set_window(window, verbose=False):
69
+ Set the extents of the integration window for each temperature.
70
+ set_center(center):
71
+ Set the central coordinate for the linecut for each temperature.
72
+ cut_data(center=None, window=None, axis=None, verbose=False):
73
+ Perform data cutting for each temperature dataset.
74
+ plot_linecuts(vertical_offset=0, **kwargs):
75
+ Plot the linecuts obtained from data cutting.
76
+ plot_linecuts_heatmap(ax=None, **kwargs):
77
+ Plot a heatmap of the linecuts obtained from data cutting.
78
+ highlight_integration_window(temperature=None, **kwargs):
79
+ Display the integration window plot for a specific temperature.
80
+ plot_integration_window(temperature=None, **kwargs):
81
+ Plot the integration window cross-sections for a specific temperature.
82
+ set_model_components(model_components):
83
+ Set the model components for all line cut models.
84
+ set_param_hint(*args, **kwargs):
85
+ Set parameter hints for all line cut models.
86
+ make_params():
87
+ Create parameters for all line cut models.
88
+ guess():
89
+ Make initial parameter guesses for all line cut models.
90
+ print_initial_params():
91
+ Print the initial parameter values for all line cut models.
92
+ plot_initial_guess():
93
+ Plot the initial guess for all line cut models.
94
+ fit(verbose=False):
95
+ Fit the line cut models for each temperature.
96
+ plot_fit(mdheadings=False, **kwargs):
97
+ Plot the fit results for each temperature.
98
+ print_fit_report():
99
+ Print the fit report for each temperature.
20
100
  """
21
101
 
22
102
  def __init__(self):
23
103
  """
24
- Initialize TempDependence class.
104
+ Initialize the TempDependence class with default values.
25
105
  """
26
- self.nxs_files_list = []
106
+
27
107
  self.sample_directory = ''
28
108
  self.xlabel = ''
29
109
  self.datasets = {}
@@ -31,58 +111,107 @@ class TempDependence:
31
111
  self.scissors = {}
32
112
  self.linecuts = {}
33
113
  self.linecutmodels = {}
114
+ self.a, self.b, self.c, self.al, self.be, self.ga, \
115
+ self.a_star, self.b_star, self.c_star, self.al_star, self.be_star, self.ga_star \
116
+ = [None] * 12
34
117
 
35
118
  def set_temperatures(self, temperatures):
119
+ """
120
+ Set the list of temperatures for the datasets.
121
+
122
+ Parameters
123
+ ----------
124
+ temperatures : list
125
+ List of temperatures to set.
126
+ """
36
127
  self.temperatures = temperatures
37
128
 
38
129
  def set_sample_directory(self, path):
130
+ """
131
+ Set the directory path where the datasets are located.
132
+
133
+ Parameters
134
+ ----------
135
+ path : str
136
+ Path to the sample directory.
137
+ """
39
138
  self.sample_directory = os.path.normpath(path)
40
139
 
41
140
  def initialize(self):
141
+ """
142
+ Initialize Scissors and LinecutModel objects for each temperature.
143
+ """
42
144
  for temperature in self.temperatures:
43
145
  self.scissors[temperature] = Scissors()
44
146
  self.scissors[temperature] = LinecutModel()
45
147
 
46
148
  def set_data(self, temperature, data):
149
+ """
150
+ Set the dataset for a specific temperature.
151
+
152
+ Parameters
153
+ ----------
154
+ temperature : str
155
+ Temperature for which to set the data.
156
+ data : object
157
+ The dataset to be set.
158
+ """
47
159
  self.datasets[temperature] = data
48
160
 
49
161
  def load_transforms(self, temperatures_list=None):
162
+ """
163
+ Load transform datasets (from nxrefine) based on temperature.
164
+
165
+ Parameters
166
+ ----------
167
+ temperatures_list : list of int or None, optional
168
+ List of temperatures to load. If None, all available temperatures are loaded.
169
+ """
50
170
  # Convert all temperatures to strings
51
171
  if temperatures_list:
52
172
  temperatures_list = [str(t) for t in temperatures_list]
53
173
 
54
- # Search all files in the sample directory
174
+ # Identify files to load
175
+ items_to_load = []
176
+ # Search for nxrefine .nxs files
55
177
  for item in os.listdir(self.sample_directory):
178
+ pattern = r'_(\d+)\.nxs'
179
+ match = re.search(pattern, item)
180
+ if match:
181
+ print(f'Found {item}')
182
+ # Identify temperature
183
+ temperature = match.group(1)
184
+ # print(f'Temperature = {temperature}')
185
+ if (temperatures_list is None) or (temperature in temperatures_list):
186
+ # Prepare file to be loaded
187
+ self.temperatures.append(temperature)
188
+ items_to_load.append(item)
189
+ # print(f'Preparing to load {temperature} K data: {item}')
56
190
 
57
- # Find the nxs files
58
- if item.endswith('.nxs'):
191
+ # Convert all temperatures to int temporarily to sort temperatures list before loading
192
+ self.temperatures = [int(t) for t in self.temperatures]
193
+ self.temperatures.sort()
194
+ self.temperatures = [str(t) for t in self.temperatures]
59
195
 
60
- # Load the nxs files
61
- path = os.path.join(self.sample_directory, item)
62
- g = nxload(path)
196
+ for i, item in enumerate(items_to_load):
197
+ path = os.path.join(self.sample_directory, item)
63
198
 
64
- # Extract the sample temperature
65
- temperature = str(int(np.round(g.entry.sample.temperature.nxdata)))
199
+ # Save dataset
200
+ self.datasets[self.temperatures[i]] = load_transform(path)
66
201
 
67
- # Load all samples (or a subset if temperatures_list is provided)
68
- if (temperatures_list is None) or (temperature in temperatures_list):
69
- self.temperatures.append(temperature)
70
- self.datasets[temperature] = load_transform(path)
71
- # Initialize scissors object at each temperature
72
- self.scissors[temperature] = Scissors()
73
- self.scissors[temperature].set_data(self.datasets[temperature])
202
+ # Initialize scissors object
203
+ self.scissors[self.temperatures[i]] = Scissors()
204
+ self.scissors[self.temperatures[i]].set_data(self.datasets[self.temperatures[i]])
74
205
 
75
- # Initialize linecutmodel object at each temperature
76
- self.linecutmodels[temperature] = LinecutModel()
206
+ # Initialize linecutmodel object
207
+ self.linecutmodels[self.temperatures[i]] = LinecutModel()
77
208
 
78
209
  def load_datasets(self, file_ending='hkli.nxs', temperatures_list=None):
79
210
  """
80
- Load scattering datasets from the specified folder.
211
+ Load datasets (CHESS format) from the specified folder.
81
212
 
82
213
  Parameters
83
214
  ----------
84
- folder : str
85
- The path to the folder where the datasets are located.
86
215
  file_ending : str, optional
87
216
  The file extension of the datasets to be loaded. The default is 'hkli.nxs'.
88
217
  temperatures_list : list of int or None, optional
@@ -131,8 +260,8 @@ class TempDependence:
131
260
 
132
261
  Returns
133
262
  -------
134
- str:
135
- The folder path.
263
+ str
264
+ The folder path.
136
265
  """
137
266
  return self.sample_directory
138
267
 
@@ -142,26 +271,39 @@ class TempDependence:
142
271
  """
143
272
  self.datasets = {}
144
273
 
145
- def set_Lattice_params(self, lattice_params):
274
+ def set_lattice_params(self, lattice_params):
275
+ """
276
+ Set lattice parameters and calculate reciprocal lattice parameters.
277
+
278
+ Parameters
279
+ ----------
280
+ lattice_params : tuple
281
+ Tuple containing lattice parameters (a, b, c, al, be, ga).
282
+ """
146
283
  self.a, self.b, self.c, self.al, self.be, self.ga = lattice_params
147
- self.a_star, self.b_star, self.c_star, self.al_star, self.be_star, self.ga_star = reciprocal_lattice_params(lattice_params)
148
- def set_window(self, window):
284
+ self.a_star, self.b_star, self.c_star, \
285
+ self.al_star, self.be_star, self.ga_star = reciprocal_lattice_params(lattice_params)
286
+
287
+ def set_window(self, window, verbose=False):
149
288
  """
150
- Set the extents of the integration window.
289
+ Set the extents of the integration window for each temperature.
151
290
 
152
291
  Parameters
153
292
  ----------
154
293
  window : tuple
155
294
  Extents of the window for integration along each axis.
295
+ verbose : bool, optional
296
+ Enables printout of linecut axis and integrated axes. Default is False.
156
297
  """
157
298
  for T in self.temperatures:
158
- print("----------------------------------")
159
- print("T = " + T + " K")
160
- self.scissors[T].set_window(window)
299
+ if verbose:
300
+ print("----------------------------------")
301
+ print("T = " + T + " K")
302
+ self.scissors[T].set_window(window, verbose)
161
303
 
162
304
  def set_center(self, center):
163
305
  """
164
- Set the central coordinate for the linecut.
306
+ Set the central coordinate for the linecut for each temperature.
165
307
 
166
308
  Parameters
167
309
  ----------
@@ -171,39 +313,46 @@ class TempDependence:
171
313
  for T in self.temperatures:
172
314
  self.scissors[T].set_center(center)
173
315
 
174
- def cut_data(self, center=None, window=None, axis=None):
316
+ def cut_data(self, center=None, window=None, axis=None, verbose=False):
175
317
  """
176
318
  Perform data cutting for each temperature dataset.
177
319
 
178
320
  Parameters
179
321
  ----------
180
- center : tuple
322
+ center : tuple, optional
181
323
  The center point for cutting the data.
182
- window : tuple
324
+ Defaults to the first temperature's center if None.
325
+ window : tuple, optional
183
326
  The window size for cutting the data.
327
+ Defaults to the first temperature's window if None.
184
328
  axis : int or None, optional
185
- The axis along which to perform the cutting. If None, cutting is performed along the
186
- longest axis in `window`. The default is None.
329
+ The axis along which to perform the cutting.
330
+ Defaults to the longest axis in `window` if None.
331
+ verbose : bool, optional
332
+ Enables printout of linecut progress. Default is False.
187
333
 
188
334
  Returns
189
335
  -------
190
- list
191
- A list of linecuts obtained from the cutting operation.
336
+ dict
337
+ A dictionary of linecuts obtained from the cutting operation.
192
338
  """
193
339
 
194
340
  center = center if center is not None else self.scissors[self.temperatures[0]].center
195
341
  window = window if window is not None else self.scissors[self.temperatures[0]].window
342
+ axis = axis if axis is not None else self.scissors[self.temperatures[0]].axis
196
343
 
197
344
  for T in self.temperatures:
198
- print("-------------------------------")
199
- print("Cutting T = " + T + " K data...")
200
- self.scissors[T].cut_data(center, window, axis)
345
+ if verbose:
346
+ print("-------------------------------")
347
+ print("Cutting T = " + T + " K data...")
348
+ self.scissors[T].cut_data(center, window, axis, verbose)
201
349
  self.linecuts[T] = self.scissors[T].linecut
202
350
  self.linecutmodels[T].set_data(self.linecuts[T])
203
351
 
204
352
  xlabel_components = [self.linecuts[self.temperatures[0]].axes
205
353
  if i == self.scissors[self.temperatures[0]].axis
206
- else str(c) for i, c in enumerate(self.scissors[self.temperatures[0]].center)]
354
+ else str(c) for i, c in
355
+ enumerate(self.scissors[self.temperatures[0]].center)]
207
356
  self.xlabel = ' '.join(xlabel_components)
208
357
 
209
358
  return self.linecuts
@@ -245,10 +394,53 @@ class TempDependence:
245
394
 
246
395
  return fig, ax
247
396
 
397
+ def plot_linecuts_heatmap(self, ax=None, **kwargs):
398
+ """
399
+ Plot the linecuts obtained from data cutting.
400
+
401
+ Parameters
402
+ ----------
403
+ ax : matplotlib.axes.Axes, optional
404
+ The axes on which to plot the heatmap. If None, a new figure and axes
405
+ are created. The default is None.
406
+ **kwargs
407
+ Additional keyword arguments to be passed to the `pcolormesh` function.
408
+
409
+ Returns
410
+ -------
411
+ QuadMesh
412
+ The plotted heatmap object.
413
+ """
414
+
415
+ # Retrieve linecut data for the first temperature and extract x-axis data
416
+ cut = self.linecuts[self.temperatures[0]]
417
+ x = cut[cut.axes].nxdata
418
+
419
+ # Convert the list of temperatures to a NumPy array for the y-axis
420
+ y = np.array([int(t) for t in self.temperatures])
421
+
422
+ # Collect counts from each temperature and ensure they are numpy arrays
423
+ v = [self.linecuts[T].counts.nxdata for T in self.temperatures]
424
+
425
+ # Convert list of arrays to a 2D array for the heatmap
426
+ v_2d = np.array(v)
427
+
428
+ # Create the grid for the heatmap
429
+ X, Y = np.meshgrid(x, y)
430
+
431
+ # Plot using pcolormesh
432
+ if ax is None:
433
+ _, ax = plt.subplots()
434
+ p = ax.pcolormesh(X, Y, v_2d, **kwargs)
435
+ plt.colorbar(p, label='counts')
436
+ ax.set(xlabel=cut.axes, ylabel=r'$T$ (K)')
437
+
438
+ return p
439
+
248
440
  def highlight_integration_window(self, temperature=None, **kwargs):
249
441
  """
250
- Displays the integration window plot for a specific temperature, or for the first temperature if
251
- none is provided.
442
+ Displays the integration window plot for a specific temperature,
443
+ or for the first temperature if none is provided.
252
444
 
253
445
  Parameters
254
446
  ----------
@@ -261,8 +453,10 @@ class TempDependence:
261
453
  """
262
454
 
263
455
  if temperature is not None:
264
- p = self.scissors[self.temperatures[0]].highlight_integration_window(data=self.datasets[temperature],
265
- **kwargs)
456
+ p = self.scissors[
457
+ self.temperatures[0]].highlight_integration_window(
458
+ data=self.datasets[temperature], **kwargs
459
+ )
266
460
  else:
267
461
  p = self.scissors[self.temperatures[0]].highlight_integration_window(
268
462
  data=self.datasets[self.temperatures[0]], **kwargs
@@ -272,15 +466,18 @@ class TempDependence:
272
466
 
273
467
  def plot_integration_window(self, temperature=None, **kwargs):
274
468
  """
275
- Plots the three principal cross-sections of the integration volume on a single figure for a specific
276
- temperature, or for the first temperature if none is provided.
469
+ Plots the three principal cross-sections of the integration volume on
470
+ a single figure for a specific temperature, or for the first temperature
471
+ if none is provided.
277
472
 
278
473
  Parameters
279
474
  ----------
280
475
  temperature : str, optional
281
- The temperature at which to plot the integration volume. If provided, the plot
282
- will be generated using the dataset corresponding to the specified temperature. If not
283
- provided, the integration window plots will be generated for the first temperature.
476
+ The temperature at which to plot the integration volume. If provided,
477
+ the plot will be generated using the dataset corresponding to the
478
+ specified temperature. If not provided, the integration window plots
479
+ will be generated for the first temperature.
480
+
284
481
  **kwargs : keyword arguments, optional
285
482
  Additional keyword arguments to customize the plot.
286
483
  """
@@ -296,9 +493,9 @@ class TempDependence:
296
493
  """
297
494
  Set the model components for all line cut models.
298
495
 
299
- This method sets the same model components for all line cut models in the analysis.
300
- It iterates over each line cut model and calls their respective `set_model_components` method
301
- with the provided `model_components`.
496
+ This method sets the same model components for all line cut models in the
497
+ analysis. It iterates over each line cut model and calls their respective
498
+ `set_model_components` method with the provided `model_components`.
302
499
 
303
500
  Parameters
304
501
  ----------
@@ -306,7 +503,8 @@ class TempDependence:
306
503
  The model components to set for all line cut models.
307
504
 
308
505
  """
309
- [linecutmodel.set_model_components(model_components) for linecutmodel in self.linecutmodels.values()]
506
+ [linecutmodel.set_model_components(model_components) for
507
+ linecutmodel in self.linecutmodels.values()]
310
508
 
311
509
  def set_param_hint(self, *args, **kwargs):
312
510
  """
@@ -324,7 +522,8 @@ class TempDependence:
324
522
  Arbitrary keyword arguments.
325
523
 
326
524
  """
327
- [linecutmodel.set_param_hint(*args, **kwargs) for linecutmodel in self.linecutmodels.values()]
525
+ [linecutmodel.set_param_hint(*args, **kwargs)
526
+ for linecutmodel in self.linecutmodels.values()]
328
527
 
329
528
  def make_params(self):
330
529
  """
@@ -349,8 +548,9 @@ class TempDependence:
349
548
  """
350
549
  Print the initial parameter values for all line cut models.
351
550
 
352
- This method prints the initial parameter values for all line cut models in the analysis.
353
- It iterates over each line cut model and calls their respective `print_initial_params` method.
551
+ This method prints the initial parameter values for all line cut models
552
+ in the analysis. It iterates over each line cut model and calls their
553
+ respective `print_initial_params` method.
354
554
 
355
555
  """
356
556
  [linecutmodel.print_initial_params() for linecutmodel in self.linecutmodels.values()]
@@ -364,22 +564,29 @@ class TempDependence:
364
564
 
365
565
  """
366
566
  for T, linecutmodel in self.linecutmodels.items():
367
- fig, ax = plt.subplots()
567
+ _, ax = plt.subplots()
368
568
  ax.set(title=T + ' K')
369
569
  linecutmodel.plot_initial_guess()
370
570
 
371
- def fit(self):
571
+ def fit(self, verbose=False):
372
572
  """
373
573
  Fit the line cut models.
374
574
 
375
575
  This method fits the line cut models for each temperature in the analysis.
376
576
  It iterates over each line cut model, performs the fit, and prints the fitting progress.
377
577
 
578
+ Parameters
579
+ ----------
580
+ verbose : bool, optional
581
+ Enables printout of fitting progress. Default False.
582
+
378
583
  """
379
584
  for T, linecutmodel in self.linecutmodels.items():
380
- print(f"Fitting {T} K data...")
585
+ if verbose:
586
+ print(f"Fitting {T} K data...")
381
587
  linecutmodel.fit()
382
- print("Done.")
588
+ if verbose:
589
+ print("Done.")
383
590
  print("Fits completed.")
384
591
 
385
592
  def plot_fit(self, mdheadings=False, **kwargs):
@@ -396,7 +603,9 @@ class TempDependence:
396
603
  if mdheadings:
397
604
  display(Markdown(f"### {T} K Fit Results"))
398
605
  # Plot fit
399
- linecutmodel.plot_fit(xlabel=self.xlabel, ylabel=self.datasets[self.temperatures[0]].signal, title=f"{T} K",
606
+ linecutmodel.plot_fit(xlabel=self.xlabel,
607
+ ylabel=self.datasets[self.temperatures[0]].signal,
608
+ title=f"{T} K",
400
609
  **kwargs)
401
610
 
402
611
  def print_fit_report(self):