1212import numpy as np
1313from PyQt6 .QtWidgets import QInputDialog , QMessageBox
1414import h5py as h5
15- from plaid .nexus import (get_nx_default , get_nx_signal , get_nx_signal_errors , get_nx_axes ,
16- get_nx_energy , get_nx_monitor , get_instrument_name , get_source_name ,
17- get_nx_sample , get_nx_transformations , get_translations_from_nx_transformations )
15+ from plaid .nexus import (get_nx_monitor , get_nx_sample , get_nx_transformations ,
16+ get_translations_from_nx_transformations )
1817from plaid .misc import q_to_tth , tth_to_q , get_map_shape_and_indices , average_blocks
19- from plaid .dialogs import H5Dialog
20-
21-
22-
18+ from plaid .io import export_xy
2319
2420class AzintData ():
2521 """
@@ -30,7 +26,6 @@ class AzintData():
3026 Attributes:
3127 - x: The radial axis data (2theta or q).
3228 - I: The intensity data.
33- - y_avg: The average intensity data.
3429 - is_q: A boolean indicating if the radial axis is in q or 2theta.
3530 - E: The energy data, if available.
3631 - I0: The I0 data, if available.
@@ -47,7 +42,7 @@ def __init__(self, parent=None,fnames=None):
4742 self .x = None
4843 self .I = None
4944 self .I_error = None
50- self .y_avg = None
45+ # self.y_avg = None
5146 self .is_q = False
5247 self .E = None
5348 self .I0 = None
@@ -64,84 +59,16 @@ def __init__(self, parent=None,fnames=None):
6459
6560 #self.aux_data = {} # {alias: np.array}
6661
67-
68- def load (self , look_for_I0 = True ):
69- """
70- Determine the file type and load the data with the appropriate function.
71- The load function should take a file name as input and return the x, I, I_error, is_q, and E values.
72- If the I_error or energy are not available in the file, the load function should return None for both.
73- Parameters:
74- - look_for_I0: If True, attempts to load I0 data from a nxmonitor dataset in the file(s).
75- Returns:
76- - True if the data was loaded successfully, False otherwise.
77- """
78- messages = [] # messages to return to caller. A workaround for threading issues with QMessageBox
79- if not all (fname .endswith ('.h5' ) for fname in self .fnames ):
80- messages .append ((QMessageBox .critical ,"Error" , "File(s) are not HDF5 files." ))
81- print ("File(s) are not HDF5 files." )
82- return False , messages
83-
84- if self ._load_func is None :
85- # Determine the load function based on the first file
86- self ._determine_load_func (self .fnames [0 ])
87- if self ._load_func is None :
88- messages .append ((QMessageBox .critical ,"Error" , "No valid load function found. Please provide a valid azimuthal integration file." ))
89- print ("No valid load function found. Please provide a valid azimuthal integration file." )
90- return False , messages
91-
92- x = None
93- I = np .array ([[],[]])
94- I_error = np .array ([[],[]])
95- _shapes = []
96- for fname in self .fnames :
97- x_ , I_ , I_error_ , is_q , E = self ._load_func (fname )
98- if x_ is None or I_ is None :
99- messages .append ((QMessageBox .critical ,"Error" , f"Error loading data from { fname } ." ))
100- print (f"Error loading data from { fname } ." )
101- return False , messages
102- if x is not None and x_ .shape != x .shape :
103- print (f"Error: Inconsistent x shapes in { fname } ." )
104- messages .append ((QMessageBox .critical ,"Error" , f"Inconsistent x shapes in { fname } ." ))
105- #QMessageBox.critical(self.parent, "Error", f"Inconsistent x shapes in {fname}.")
106- return False , messages
107- x = x_
108- I = np .append (I , I_ , axis = 0 ) if I .size else I_
109- _shapes .append (I_ .shape )
110- if I_error_ is not None :
111- I_error = np .append (I_error , I_error_ , axis = 0 ) if I_error .size else I_error_
112- if I_error .size == 0 :
113- I_error = None
114- #I = np.array(I)
115- self .x = x
116- self .I = I
117- self .I_error = I_error
62+ def set_secondary_data (self , data_dict ):
63+ """Set the "secondary" azimuthal integration data from a dictionary."""
64+ is_q = data_dict ["q" ] is not None
11865 self .is_q = is_q
119- self .E = E
120- self .y_avg = I .mean (axis = 0 )
121- self .shape = I .shape
122- self ._shapes = _shapes
123-
124- if look_for_I0 and self ._load_func == self ._load_azint :
125- # If the data is loaded from a nxazint file, attempts to load
126- # the I0 data from a nxmonitor dataset in the file. Give the user
127- # the option ignore the I0 data
128- if self .load_I0_from_nxmonitor ():
129- # reply = QMessageBox.question(None, "NXmonitor data found",
130- # "I0 data loaded from nxmonitor dataset. Do you want to use it?",
131- # QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
132- # QMessageBox.StandardButton.Yes)
133- # if reply == QMessageBox.StandardButton.No:
134- # self.I0 = None
135- messages .append ((QMessageBox .question ,"NXmonitor data found" ,
136- "I0 data loaded from nxmonitor dataset. Do you want to use it?" ,
137- QMessageBox .StandardButton .Yes | QMessageBox .StandardButton .No ,
138- QMessageBox .StandardButton .Yes ))
139- if self ._load_func == self ._load_azint :
140- # If the data is loaded from a nxazint file, attempts to load
141- # the map shape and pixel indices from a nxtransformations group in the file.
142- self .load_map_shape_and_indices ()
143-
144- return True , messages
66+ self .x = data_dict ["q" ] if is_q else data_dict ["tth" ]
67+ self .E = data_dict .get ("energy" , None )
68+ self .instrument_name = data_dict .get ("instrument_name" , None )
69+ self .source_name = data_dict .get ("source_name" , None )
70+ self .map_shape = data_dict .get ("map_shape" , None )
71+ self .map_indices = data_dict .get ("map_indices" , None )
14572
14673 def load_I0_from_nxmonitor (self ):
14774 """
@@ -203,7 +130,7 @@ def reduce_data(self, reduction_factor=2, axes=(0,)):
203130 self .I_error = average_blocks (self .I_error , reduction_factor = reduction_factor , axes = axes )
204131 if self .I0 is not None :
205132 self .I0 = average_blocks (self .I0 , reduction_factor = reduction_factor , axes = axes )
206- self .y_avg = self .I .mean (axis = 0 ) if self .I is not None else None
133+ # self.y_avg = self.I.mean(axis=0) if self.I is not None else None
207134 self .shape = self .I .shape if self .I is not None else None
208135 self .map_shape , self .map_indices = None , None # Invalidate map shape and indices after reduction
209136 self .reduction_factor *= reduction_factor
@@ -328,17 +255,6 @@ def set_I0(self, I0):
328255 print ("I0 data must be a numpy array or a list/tuple." )
329256 return
330257
331- # # check if the I0 data are close to unity
332- # # otherwise, normalize it and print a warning
333- # if self.I0.min() <= 0 or self.I0.max() < 0.5 or self.I0.max() > 2:
334- # message = ("Warning: I0 data should be close to unity and >0. Normalizing it.")
335- # QMessageBox.warning(self.parent, "I0 Data Warning", message)
336-
337- # print(f"I0 [{self.I0.min():.2e}, {self.I0.max():.2e}] normalized to [{self.I0.min()/self.I0.max():.2f}, 1.00]")
338- # self.I0 = self.I0 / np.max(self.I0)
339- # self.I0[self.I0<=0] = 1 # Set any zero values to 1 to avoid division by zero
340-
341-
342258 if self .I is None :
343259 # Don't normalize (yet)
344260 return
@@ -366,7 +282,7 @@ def export_pattern(self, fname, index, is_Q=False, I0_normalized=True, kwargs={}
366282 print ("Error retrieving data for export." )
367283 return False
368284
369- self . _export_xy (fname ,x ,y ,y_e , kwargs )
285+ export_xy (fname ,x ,y ,y_e , kwargs )
370286 return True
371287
372288 def export_average_pattern (self , fname , is_Q = False , I0_normalized = True , bgr_subtracted = True , kwargs = {}):
@@ -389,7 +305,7 @@ def export_average_pattern(self, fname, is_Q=False, I0_normalized=True, bgr_subt
389305 print ("Error retrieving data for export." )
390306 return False
391307
392- self . _export_xy (fname ,x ,y ,y_e , kwargs )
308+ export_xy (fname ,x ,y ,y_e , kwargs )
393309 return True
394310
395311 def get_info_string (self ):
@@ -415,147 +331,20 @@ def get_info_string(self):
415331 name += f"reduced x{ self .reduction_factor } "
416332 return name
417333
418- def _determine_load_func (self , fname ):
419- """Determine the appropriate load function based on the file structure."""
420- with h5 .File (fname , 'r' ) as f :
421- if 'entry/data' in f :
422- self ._load_func = self ._load_azint
423- elif 'entry/data1d' in f :
424- self ._load_func = self ._load_azint_old
425- elif 'entry/dataxrd1d' in f :
426- self ._load_func = self ._load_DM_map
427- elif 'I' in f :
428- self ._load_func = self ._load_DM_old
429- else :
430- # Attempt to load using the H5Dialog if no specific structure is found
431- self ._load_func = self ._load_dialog
432- #print("File type not recognized. Please provide a valid azimuthal integration file.")
433- #self._load_func = None
434-
435- def _load_azint (self , fname ):
436- """Load azimuthal integration data from a nxazint HDF5 file."""
437- with h5 .File (fname , 'r' ) as f :
438- default = get_nx_default (f )
439- if default is None :
440- print (f"File { fname } does not contain a valid azimuthal integration dataset." )
441- return None , None , None , None
442- signal = get_nx_signal (default )
443- signal_errors = get_nx_signal_errors (default )
444- axis = get_nx_axes (default )[- 1 ] # Get the last axis, which is usually the radial axis
445- if signal is None or axis is None :
446- print (f"File { fname } does not contain a valid azimuthal integration dataset." )
447- return None , None , None , None
448- x = axis [:]
449- is_Q = 'q' in axis .attrs ['long_name' ].lower () if 'long_name' in axis .attrs else False
450- I = signal [:]
451- I_error = signal_errors [:] if signal_errors is not None else None
452- E = get_nx_energy (f )
453-
454- # get the instrument and source names if available
455- self .instrument_name = get_instrument_name (f )
456- self .source_name = get_source_name (f )
457- return x , I , I_error , is_Q , E
458- # with h5.File(fname, 'r') as f:
459- # data_group = f['entry/data']
460- # x = data_group['radial_axis'][:]
461- # I = data_group['I'][:]
462- # is_q = 'q' in data_group['radial_axis'].attrs['long_name'].lower()
463-
464- # if 'entry/instrument/monochromator/energy' in f:
465- # E = f['entry/instrument/monochromator/energy'][()]
466- # elif 'entry/instrument/monochromator/wavelength' in f:
467- # wavelength = f['entry/instrument/monochromator/wavelength'][()]
468- # E = 12.398 / wavelength # Convert wavelength to energy in keV
469- # else:
470- # E = None
471- # return x, I, is_q, E
472-
473- def _load_azint_old (self , fname ):
474- """Load azimuthal integration data from an old (DanMAX) nxazint HDF5 file."""
475- with h5 .File (fname , 'r' ) as f :
476- data_group = f ['entry/data1d' ]
477- if '2th' in data_group :
478- x = data_group ['2th' ][:]
479- is_q = False
480- elif 'q' in data_group :
481- x = data_group ['q' ][:]
482- is_q = True
483- I = data_group ['I' ][:]
484- return x , I , None , is_q , None
485-
486- def _load_DM_old (self , fname ):
487- """Load azimuthal integration data from an old DanMAX HDF5 file."""
488- with h5 .File (fname , 'r' ) as f :
489- if '2th' in f :
490- x = f ['2th' ][:]
491- is_q = False
492- elif 'q' in f :
493- x = f ['q' ][:]
494- is_q = True
495- I = f ['I' ][:]
496- return x , I , None , is_q , None
497-
498- def _load_DM_map (self , fname ):
499- """Load azimuthal integration data from a DanMAX map HDF5 file."""
500- with h5 .File (fname , 'r' ) as f :
501- data_group = f ['entry/dataxrd1d' ]
502- if 'tth' in data_group :
503- x = data_group ['tth' ][:]
504- is_q = False
505- elif 'q' in data_group :
506- x = data_group ['q' ][:]
507- is_q = True
508- I = data_group ['xrd' ][:] # [radial bins, fast_axis, slow_axis]
509- self .map_shape = I .shape [1 :] # (fast_axis, slow_axis)
510- # self.map_indices = np.arange(I.shape[1]*I.shape[2])
511- self .map_indices = list (range (I .shape [1 ]* I .shape [2 ]))
512- # transpose and reshape I
513- I = np .transpose (I , (1 ,2 ,0 )).reshape (- 1 , I .shape [0 ]) # [num_patterns, radial bins]
514- # in the case of xrd-ct data, an extra first column might be present as absorption data
515- # in that case, remove it
516- if I .shape [1 ] - x .shape [0 ] == 1 :
517- I = I [:, 1 :] # Remove first column if x has one less element than I
518- return x , I , None , is_q , None
519-
520- def _load_dialog (self , fname ):
521- """
522- Load azimuthal integration data from an h5 file dialog.
523- This function is used as a last resort if no other load function is found.
524- """
525- dialog = H5Dialog (self .parent , fname )
526- if not dialog .exec_1d_2d_pair ():
527- return None , None , None , None , None
528-
529- selected = dialog .get_selected_items () # list of tuples with (alias, full_path, shape)
530- axis = [item for item in selected if not "×" in item [2 ]][0 ]
531- signal = [item for item in selected if "×" in item [2 ]][0 ]
532- # Check if the shape of the axis and signal match
533- if not axis [2 ] in signal [2 ].split ("×" )[1 ]:
534- QMessageBox .critical (self .parent , "Error" , f"Error: The shape of the axis { axis [2 ]} does not match the shape of the signal { signal [2 ]} ." )
535- return None , None , None , None
536- with h5 .File (fname , 'r' ) as f :
537- x = f [axis [1 ]][:]
538- I = f [signal [1 ]][:]
539- # attempt to guess if the axis is q or 2theta
540- is_q = 'q' in axis [0 ].lower () or 'q' in f [axis [1 ]].attrs .get ('long_name' , '' ).lower ()
541- return x , I , None , is_q , None
542-
543- def _export_xy (self , fname , x , y , y_e = None , kwargs = {}):
544- """
545- Export the azimuthal integration data to a text file.
546- kwargs are passed to np.savetxt.
547- """
548- if y_e is None :
549- np .savetxt (fname , np .column_stack ((x , y )),comments = '#' ,** kwargs )
550- else :
551- np .savetxt (fname , np .column_stack ((x , y , y_e )),comments = '#' ,** kwargs )
552- return True
553-
554334class AuxData :
555335 """A class to hold auxiliary data for azimuthal integration."""
556336 def __init__ (self ,parent = None ):
557337 self ._parent = parent
558338 self .I0 = None
339+ self ._E = None
340+
341+ def set_energy (self , E ):
342+ """Set energy"""
343+ self ._E = E
344+
345+ def get_energy (self ):
346+ """Get energy"""
347+ return self ._E
559348
560349 def set_I0 (self , I0 ):
561350 """Set I0"""
0 commit comments