1+ """
2+ Instrument Model from NeXus files
3+
4+ Makes use of NeXus classes and NXtransformations to build a structure of the instrument from a scan file.
5+ """
6+
17import h5py
28import numpy as np
3- from matplotlib import pyplot as plt
49
510from mmg_toolbox .nexus import nexus_names as nn
611from mmg_toolbox .nexus .nexus_functions import nx_find , get_dataset_value
712from mmg_toolbox .nexus .nexus_transformations import nx_direction , nx_transformations_max_size , \
813 nx_transformations_matrix , nx_transform_vector
914from mmg_toolbox .utils .rotations import norm_vector , transform_by_t_matrix
1015from mmg_toolbox .utils .xray_utils import wavevector , photon_energy , photon_wavelength , bmatrix
16+ from mmg_toolbox .plotting .matplotlib import Axes3D
1117
18+ # types
19+ Shape = tuple [int , int , int ] # (n, i, j) == (frame, slow_pixel, fast_pixel) #TODO: should this be fast, slow?
20+ Pixel = tuple [int , float , float ] # (n, i, j) pixel coordinates
21+
22+ #TODO: add polarisation
23+ #TODO: improve docs
1224
1325class NXBeam :
1426 """
@@ -31,18 +43,10 @@ def energy_wavelength(self):
3143 :return: incident_energy, incident_wavelength
3244 """
3345 if nn .NX_WL in self .beam :
34- dataset = self .beam [nn .NX_WL ]
35- units = dataset .attrs .get (nn .NX_UNITS , b'nm' ).decode ()
36- wl = dataset [()]
37- if units == 'nm' :
38- wl = 10 * wl # wavelength in Angstroms
46+ wl = get_dataset_value (nn .NX_WL , self .beam , units = 'A' )
3947 return photon_energy (wl ), wl
4048 elif nn .NX_EN in self .beam :
41- dataset = self .beam [nn .NX_WL ]
42- units = dataset .attrs .get (nn .NX_UNITS , b'ev' ).decode ()
43- en = dataset [()]
44- if units .lower () == 'ev' :
45- en = en / 1000. # wavelength in keV
49+ en = get_dataset_value (nn .NX_EN , self .beam , units = 'keV' )
4650 return en , photon_wavelength (en )
4751 else :
4852 raise KeyError (f"{ self .beam } contains no '{ nn .NX_WL } ' or '{ nn .NX_EN } '" )
@@ -75,14 +79,15 @@ def __init__(self, path: str, hdf_file: h5py.File):
7579 def __repr__ (self ):
7680 return f"NXSsample({ self .sample } )"
7781
78- def hkl2q (self , hkl : tuple [float , float , float ] | np .ndarray ) :
82+ def hkl2q (self , hkl : tuple [float , float , float ] | np .ndarray , index : int = 0 ) -> np . ndarray :
7983 """
80- Returns wavecector direction for given hkl
81- :param hkl: Miller indices, in units of reciprocal lattice vectors
84+ Returns wavevector direction for given hkl in lab space
85+ :param hkl: Miller indices (h, k, l), in units of reciprocal lattice vectors
86+ :param index: index of scan
8287 :return: Q position in inverse Angstroms
8388 """
8489 hkl = np .reshape (hkl , (- 1 , 3 ))
85- z = self .transforms [0 ][:3 , :3 ]
90+ z = self .transforms [index ][:3 , :3 ]
8691 ub = 2 * np .pi * self .ub_matrix
8792 return np .dot (z , np .dot (ub , hkl .T )).T
8893
@@ -121,7 +126,7 @@ def __init__(self, path: str, hdf_file: h5py.File):
121126 def __repr__ (self ):
122127 return f"NXDetectorModule({ self .module } )"
123128
124- def shape (self ):
129+ def shape (self ) -> Shape :
125130 """
126131 Return scan shape of module
127132 (n, i, j)
@@ -132,26 +137,26 @@ def shape(self):
132137 """
133138 return self .size , self .data_size [0 ], self .data_size [1 ]
134139
135- def pixel_wavevector (self , point : tuple [ int , int , int ] , wavelength_a ) -> np .ndarray :
140+ def pixel_wavevector (self , point : Pixel , wavelength_a ) -> np .ndarray :
136141 """
137- Return wavevector of pixel
142+ Return wavevector of pixel in lab frame
138143 :param point: (n, i, j) == (frame, slow_axis_pixel, fast_axis_pixel)
139144 :param wavelength_a: wavelength in Angstrom
140- :return: [dx, dy, dz] unit vector
145+ :return: [x, y, z] in inverse Angstrom
141146 """
142147 return wavevector (wavelength_a ) * self .pixel_direction (point )
143148
144- def pixel_direction (self , point : tuple [ int , int , int ] ) -> np .ndarray :
149+ def pixel_direction (self , point : Pixel ) -> np .ndarray :
145150 """
146- Return direction of pixel
151+ Return direction of pixel in lab frame
147152 :param point: (n, i, j) == (frame, slow_axis_pixel, fast_axis_pixel)
148153 :return: [dx, dy, dz] unit vector
149154 """
150155 return norm_vector (self .pixel_position (point ))
151156
152- def pixel_position (self , point : tuple [ int , int , int ] ) -> np .ndarray :
157+ def pixel_position (self , point : Pixel ) -> np .ndarray :
153158 """
154- Return position of pixel (n, i, j)
159+ Return position of pixel (n, i, j) in lab frame
155160 n = frame in scan
156161 i = pixel along slow axis
157162 j = pixel along fast axis
@@ -167,6 +172,7 @@ def pixel_position(self, point: tuple[int, int, int]) -> np.ndarray:
167172 return np .squeeze (ii * slow_direction + jj * fast_direction + module_origin )
168173
169174 def corners (self , frame : int ) -> np .ndarray :
175+ """return corners of the detector module at this frame in scan"""
170176 shape = self .shape ()
171177 corners = np .vstack ([
172178 self .pixel_position ((frame , 0 , 0 )), # module origin
@@ -200,9 +206,9 @@ def __repr__(self):
200206 return f"NXDetector({ self .detector } ) with { len (self .modules )} modules"
201207
202208
203- class NXScan :
209+ class NXInstrumentModel :
204210 """
205- NXScan object
211+ NXInstrumentModel object
206212 """
207213
208214 def __init__ (self , hdf_file : h5py .File ):
@@ -228,26 +234,47 @@ def __init__(self, hdf_file: h5py.File):
228234
229235 sample_obj = nx_find (self .entry , nn .NX_SAMPLE )
230236 self .sample = NXSsample (sample_obj .name , hdf_file )
231- beam_obj = nx_find (self . sample_obj , nn .NX_BEAM )
237+ beam_obj = nx_find (sample_obj , nn .NX_BEAM )
232238 self .beam = NXBeam (beam_obj .name , hdf_file )
233239
234240 def __repr__ (self ):
235- return f"NXScan ({ self .file } )"
241+ return f"NXInstrumentModel ({ self .file } )"
236242
237- def shape (self ):
238- detector_module = self .detectors [0 ].modules [0 ]
243+ def _first_detector (self ):
244+ return self .detectors [0 ].modules [0 ]
245+
246+ def shape (self ) -> Shape :
247+ """
248+ Return scan shape from the first detector module
249+ (n, i, j)
250+ Where:
251+ n = frames in scan
252+ i = pixels along slow axis
253+ j = pixels along fast axis
254+ """
255+ detector_module = self ._first_detector ()
239256 return detector_module .shape ()
240257
241- def detector_q (self , point : tuple [int , int , int ] = (0 , 0 , 0 )):
258+ def detector_q (self , point : Pixel = (0 , 0 , 0 )) -> np .ndarray :
259+ """
260+ return wavevector transfer, kf-ki, of first detector module, in lab frame
261+ :param point: (n, i, j) == (frame, slow_axis_pixel, fast_axis_pixel)
262+ :return: [x, y, z] in inverse Angstrom
263+ """
242264 wavelength = self .beam .wl
243265 ki = self .beam .incident_wavevector
244- detector_module = self .detectors [ 0 ]. modules [ 0 ]
266+ detector_module = self ._first_detector ()
245267 kf = detector_module .pixel_wavevector (point , wavelength )
246268 return kf - ki
247269
248- def hkl (self , point : tuple [int , int , int ] = (0 , 0 , 0 )):
270+ def hkl (self , point : Pixel = (0 , 0 , 0 )):
271+ """
272+ return wavevector transfer, Q, of pixel coordinate
273+ :param point: (n, i, j) == (frame, slow_axis_pixel, fast_axis_pixel)
274+ :return: [x, y, z] in inverse Angstrom
275+ """
249276 q = self .detector_q (point )
250- z = self .sample .transforms [- 1 ] [:3 , :3 ] #TODO: should this chain all transforms?
277+ z = self .sample .transforms [point [ 0 ]] [:3 , :3 ]
251278 ub = 2 * np .pi * self .sample .ub_matrix
252279
253280 inv_ub = np .linalg .inv (ub )
@@ -264,98 +291,89 @@ def hkl2q(self, hkl: tuple[float, float, float] | np.ndarray):
264291 """
265292 return self .sample .hkl2q (hkl )
266293
267- def plot_instrument (self , figsize = [16 , 6 ], dpi = 100 ):
268- fig = plt .figure (figsize = figsize , dpi = dpi )
269- ax = fig .add_subplot (projection = '3d' )
270-
294+ def plot_instrument (self , axes : Axes3D ):
271295 instrument_name = get_dataset_value ('name' , self .instrument , 'no name' )
272296 max_distance = max ([np .linalg .norm (position ) for position in self .component_positions .values ()])
273297 max_position = max_distance * self .beam .direction
274298
275- ax .plot ([- max_position [0 ], 0 ], [- max_position [2 ], 0 ], [- max_position [1 ], 0 ], 'k-' ) # beam
299+ axes .plot ([- max_position [0 ], 0 ], [- max_position [2 ], 0 ], [- max_position [1 ], 0 ], 'k-' ) # beam
276300 beam_cont = np .linalg .norm (self .detectors [0 ].position ) * self .beam .direction
277- ax .plot ([0 , beam_cont [0 ]], [0 , beam_cont [2 ]], [0 , beam_cont [1 ]], 'k:' ) # continued beam
301+ axes .plot ([0 , beam_cont [0 ]], [0 , beam_cont [2 ]], [0 , beam_cont [1 ]], 'k:' ) # continued beam
278302 # detectors
279303 for detector in self .detectors :
280304 pos = detector .position
281- ax .plot ([0 , pos [0 ]], [0 , pos [2 ]], [0 , pos [1 ]], 'k-' ) # scattered beam
305+ axes .plot ([0 , pos [0 ]], [0 , pos [2 ]], [0 , pos [1 ]], 'k-' ) # scattered beam
282306 # components
283307 for component , position in self .component_positions .items ():
284- ax .plot (position [0 ], position [2 ], position [1 ], 'r+' )
285- ax .text (position [0 ], position [2 ], position [1 ], s = component )
308+ axes .plot (position [0 ], position [2 ], position [1 ], 'r+' )
309+ axes .text (position [0 ], position [2 ], position [1 ], s = component )
286310
287- ax .set_xlabel ('X [mm]' )
288- ax .set_ylabel ('Z [mm]' )
289- ax .set_zlabel ('Y [mm]' )
290- ax .set_title (f"Instrument: { instrument_name } " )
311+ axes .set_xlabel ('X [mm]' )
312+ axes .set_ylabel ('Z [mm]' )
313+ axes .set_zlabel ('Y [mm]' )
314+ axes .set_title (f"Instrument: { instrument_name } " )
291315 # ax.set_aspect('equalxz')
292- fig .show ()
293-
294- def plot_wavevectors (self , figsize = [16 , 6 ], dpi = 100 ):
295- fig = plt .figure (figsize = figsize , dpi = dpi )
296- ax = fig .add_subplot (projection = '3d' )
297316
298- pixel_centre = tuple ([i // 2 for i in self .shape ()])
317+ def plot_wavevectors (self , axes : Axes3D ):
318+ frames , ii , jj = self .shape ()
319+ pixel_centre = (frames // 2 , ii // 2 , jj // 2 )
299320 ki = self .beam .incident_wavevector
300321 detector_module = self .detectors [0 ].modules [0 ]
301322 kf = detector_module .pixel_wavevector (pixel_centre , self .beam .wl )
302323 q = kf - ki
324+ hkl = self .hkl (pixel_centre )
303325
304- ax .plot ([- ki [0 ], 0 ], [- ki [2 ], 0 ], [- ki [1 ], 0 ], '-k' )
305- ax .plot ([0 , kf [0 ]], [0 , kf [2 ]], [0 , kf [1 ]], '-k' )
306- ax .plot ([0 , q [0 ]], [0 , q [2 ]], [0 , q [1 ]], '-r' )
326+ axes .plot ([- ki [0 ], 0 ], [- ki [2 ], 0 ], [- ki [1 ], 0 ], '-k' )
327+ axes .plot ([0 , kf [0 ]], [0 , kf [2 ]], [0 , kf [1 ]], '-k' )
328+ axes .plot ([0 , q [0 ]], [0 , q [2 ]], [0 , q [1 ]], '-r' )
307329
308- shape = self .shape ()
309330 wl = self .beam .wl
310- for frame in range (shape [ 0 ] ):
331+ for frame in range (frames ):
311332 corners = np .vstack ([
312333 detector_module .pixel_wavevector ((frame , 0 , 0 ), wl ), # module origin
313- detector_module .pixel_wavevector ((frame , shape [ 1 ] , 0 ), wl ), # module origin + slow pixels
314- detector_module .pixel_wavevector ((frame , shape [ 1 ], shape [ 2 ] ), wl ), # o + slow + fast
315- detector_module .pixel_wavevector ((frame , 0 , shape [ 2 ] ), wl ), # o + fast
334+ detector_module .pixel_wavevector ((frame , ii , 0 ), wl ), # module origin + slow pixels
335+ detector_module .pixel_wavevector ((frame , ii , jj ), wl ), # o + slow + fast
336+ detector_module .pixel_wavevector ((frame , 0 , jj ), wl ), # o + fast
316337 detector_module .pixel_wavevector ((frame , 0 , 0 ), wl ), # module origin
317338 ])
318- ax .plot (corners [:, 0 ], corners [:, 2 ], corners [:, 1 ], '-k' )
339+ axes .plot (corners [:, 0 ], corners [:, 2 ], corners [:, 1 ], '-k' )
319340 corners_q = corners - ki
320- ax .plot (corners_q [:, 0 ], corners_q [:, 2 ], corners_q [:, 1 ], '-r' )
341+ axes .plot (corners_q [:, 0 ], corners_q [:, 2 ], corners_q [:, 1 ], '-r' )
321342
322343 # plot Reciprocal lattice
323344 astar , bstar , cstar = self .hkl2q (np .eye (3 ))
324- ax .plot ([0 , astar [0 ]], [0 , astar [2 ]], [0 , astar [1 ]], '-g' )
325- ax .plot ([0 , bstar [0 ]], [0 , bstar [2 ]], [0 , bstar [1 ]], '-g' )
326- ax .plot ([0 , cstar [0 ]], [0 , cstar [2 ]], [0 , cstar [1 ]], '-g' )
327- ax .text (astar [0 ], astar [2 ], astar [1 ], s = 'a*' )
328- ax .text (bstar [0 ], bstar [2 ], bstar [1 ], s = 'b*' )
329- ax .text (cstar [0 ], cstar [2 ], cstar [1 ], s = 'c*' )
330-
331- ax .set_xlabel ('X' )
332- ax .set_ylabel ('Z' )
333- ax .set_zlabel ('Y' )
334- ax .set_title (f"Wavevectors\n HKL: { self .hkl (pixel_centre )} " )
335- ax .set_aspect ('equal' )
336- fig .show ()
337-
338- def plot_hkl (self , figsize = [16 , 6 ], dpi = 100 ):
339- fig = plt .figure (figsize = figsize , dpi = dpi )
340- ax = fig .add_subplot (projection = '3d' )
341-
342- shape = self .shape ()
343- pixel_centre = tuple ([i // 2 for i in shape ])
344- for frame in range (shape [0 ]):
345+ axes .plot ([0 , astar [0 ]], [0 , astar [2 ]], [0 , astar [1 ]], '-g' )
346+ axes .plot ([0 , bstar [0 ]], [0 , bstar [2 ]], [0 , bstar [1 ]], '-g' )
347+ axes .plot ([0 , cstar [0 ]], [0 , cstar [2 ]], [0 , cstar [1 ]], '-g' )
348+ axes .text (astar [0 ], astar [2 ], astar [1 ], s = 'a*' )
349+ axes .text (bstar [0 ], bstar [2 ], bstar [1 ], s = 'b*' )
350+ axes .text (cstar [0 ], cstar [2 ], cstar [1 ], s = 'c*' )
351+
352+ axes .set_xlabel ('X' )
353+ axes .set_ylabel ('Z' )
354+ axes .set_zlabel ('Y' )
355+ axes .set_title (f"Wavevectors\n HKL: { hkl } " )
356+ axes .set_aspect ('equal' )
357+
358+ def plot_hkl (self , axes : Axes3D ):
359+ frames , ii , jj = self .shape ()
360+ pixel_centre = (frames // 2 , ii // 2 , jj // 2 )
361+ hkl = self .hkl (pixel_centre )
362+ for frame in range (frames ):
345363 corners = np .vstack ([
346364 self .hkl ((frame , 0 , 0 )), # module origin
347- self .hkl ((frame , shape [ 1 ] , 0 )), # module origin + slow pixels
348- self .hkl ((frame , shape [ 1 ], shape [ 2 ] )), # o + slow + fast
349- self .hkl ((frame , 0 , shape [ 2 ] )), # o + fast
365+ self .hkl ((frame , ii , 0 )), # module origin + slow pixels
366+ self .hkl ((frame , ii , jj )), # o + slow + fast
367+ self .hkl ((frame , 0 , jj )), # o + fast
350368 self .hkl ((frame , 0 , 0 )), # module origin
351369 ])
352- ax .plot (corners [:, 0 ], corners [:, 2 ], corners [:, 1 ], '-r' )
370+ axes .plot (corners [:, 0 ], corners [:, 2 ], corners [:, 1 ], '-r' )
353371 origin = self .hkl ((0 , 0 , 0 ))
354- ax .plot (origin [0 ], origin [2 ], origin [1 ], '+k' )
355-
356- ax .set_xlabel ('H' )
357- ax .set_ylabel ('L' )
358- ax .set_zlabel ('K' )
359- ax .set_title (f"HKL: { self . hkl ( pixel_centre ) } " )
360- ax .set_aspect ('equal' )
361- fig . show ()
372+ axes .plot (origin [0 ], origin [2 ], origin [1 ], '+k' )
373+
374+ axes .set_xlabel ('H' )
375+ axes .set_ylabel ('L' )
376+ axes .set_zlabel ('K' )
377+ axes .set_title (f"HKL: { hkl } " )
378+ axes .set_aspect ('equal' )
379+
0 commit comments