Skip to content

Commit 73eb300

Browse files
committed
Add features
Add features to Experiment and NexusScan, plus other improvements. experiment.py - add Experiment.load_xas - add str, repr nexus_reader.py - add HdfMap.generate_ids - add xas_scan - add instrument model scan_plot_manager.py - add histogram
1 parent d1ea8c1 commit 73eb300

17 files changed

Lines changed: 349 additions & 190 deletions

docs/run_local_docs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""
22
Run local instance of docs for testing
3+
Run make_docs.py first
34
"""
45

56
from mkdocs.commands import serve
Lines changed: 113 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
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+
17
import h5py
28
import numpy as np
3-
from matplotlib import pyplot as plt
49

510
from mmg_toolbox.nexus import nexus_names as nn
611
from mmg_toolbox.nexus.nexus_functions import nx_find, get_dataset_value
712
from mmg_toolbox.nexus.nexus_transformations import nx_direction, nx_transformations_max_size, \
813
nx_transformations_matrix, nx_transform_vector
914
from mmg_toolbox.utils.rotations import norm_vector, transform_by_t_matrix
1015
from 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

1325
class 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\nHKL: {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\nHKL: {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+

mmg_toolbox/nexus/nexus_functions.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import h5py
88

99
from mmg_toolbox.nexus import nexus_names as nn
10-
from mmg_toolbox.nexus.nexus_names import METERS
10+
from mmg_toolbox.utils.units import METERS, unit_converter
1111
from mmg_toolbox.utils.xray_utils import photon_energy, photon_wavelength
1212

1313

@@ -196,20 +196,25 @@ def get_metadata(group: h5py.Group, *name_paths_default: tuple[str, tuple, str])
196196
return metadata
197197

198198

199-
def get_dataset_value(path: str, group: h5py.Group | h5py.File, default: np.ndarray) -> np.ndarray:
199+
def get_dataset_value(path: str, group: h5py.Group,
200+
default: str | float | np.ndarray | None = None, units: str = '') -> np.ndarray | None:
200201
"""
201202
Get value from dataset in group, or return default
202203
:param path: hdf path of dataset in group
203204
:param group: hdf group
204205
:param default: returned if path doesn't exist
205-
:return: value or default
206+
:param units: converts to given units if units in attrs
207+
:return: value or default as ndarray, or None if default is None
206208
"""
207209
if path in group:
208210
dataset = group[path]
209211
if np.issubdtype(dataset, np.number):
210-
return np.squeeze(dataset[...])
212+
data = np.squeeze(dataset[...])
213+
if units and 'units' in dataset.attrs:
214+
return unit_converter(data, bytes2str(dataset.attrs['units']), units)
215+
return data
211216
return dataset.asstr()[...]
212-
return default
217+
return None if default is None else np.asarray(default)
213218

214219

215220
def nx_beam_energy(beam: h5py.Group) -> tuple[float, float]:

mmg_toolbox/nexus/nexus_names.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,6 @@
5252
ENTRY_CLASSES = ['NXentry', 'NXsubentry']
5353
XAS_DEFINITIONS = ['NXxas', 'NXxas_new']
5454
SEARCH_ATTRS = (NX_CLASS, 'local_name') # DLS attribute 'local_name' helps match metadata to scan commands
55-
METERS = { # conversion to meters
56-
'km': 1e3, 'm': 1, 'cm': 0.1, 'mm': 1e-3,
57-
'um': 1e-6, 'μm': 1e-6, 'nm': 1e-9,
58-
'A': 1e-10, 'Å': 1e-10
59-
}
6055

6156
# Polarisation field names inside NeXus groups
6257
# See https://manual.nexusformat.org/classes/base_classes/NXbeam.html#nxbeam

0 commit comments

Comments
 (0)