Skip to content

azint_writer

NXWriter

NXWriter class for writing azimuthal integration data to NeXus HDF5 files. This class handles the creation of the NeXus hierarchy, including metadata such as instrument configuration, source details, monochromator properties, and integration parameters. It supports both 1D and 2D data formats, allowing for flexible data storage and retrieval.

Attributes:

Name Type Description
- ai

Azimuthal integrator object (should contain integration results and parameters)

- output_file (str

Path to output HDF5 file

- write_1d (bool

Whether to include 1D data in the file

- write_2d (bool

Whether to include 2D data in the file

- instrument_name (str

Name of the instrument

- source_name (str

Name of the source

- source_type (str

Type of the source (e.g., 'Synchrotron')

- source_probe (str

Type of probe (e.g., 'x-ray')

Methods:

Name Description
- write_header

Creates and writes the NeXus hierarchy for the dataset, including metadata such as instrument configuration, source details, monochromator properties, and integration parameters.

- add_data

Adds azimuthal integration data to the HDF5 file under the proper NXdata group.

- write_radial_axis

Writes radial axis information to the specified group.

Source code in azint_writer/azint_writer.py
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
class NXWriter:
    """
    NXWriter class for writing azimuthal integration data to NeXus HDF5 files.
    This class handles the creation of the NeXus hierarchy, including metadata
    such as instrument configuration, source details, monochromator properties,
    and integration parameters. It supports both 1D and 2D data formats, allowing
    for flexible data storage and retrieval.

    Attributes:
        - ai: Azimuthal integrator object (should contain integration results and parameters)
        - output_file (str): Path to output HDF5 file
        - write_1d (bool): Whether to include 1D data in the file
        - write_2d (bool): Whether to include 2D data in the file
        - instrument_name (str): Name of the instrument
        - source_name (str): Name of the source
        - source_type (str): Type of the source (e.g., 'Synchrotron')
        - source_probe (str): Type of probe (e.g., 'x-ray')

    Methods:
        - write_header: Creates and writes the NeXus hierarchy for the dataset,
            including metadata such as instrument configuration, source details,
            monochromator properties, and integration parameters.
        - add_data: Adds azimuthal integration data to the HDF5 file under the
            proper NXdata group.
        - write_radial_axis: Writes radial axis information to the specified group.
    """


    def __init__(
        self, 
        ai, 
        output_file,
        write_1d=True, 
        write_2d=True, 
        instrument_name=None, 
        source_name=None, 
        source_type=None, 
        source_probe=None
    ):
        self.ai = ai
        self.output_file = output_file
        self.write_1d = write_1d
        self.write_2d = write_2d
        self.instrument_name = instrument_name
        self.source_name = source_name
        self.source_type = source_type
        self.source_probe = source_probe

        with h5py.File(self.output_file, "w") as fh_w:
            self.fh = fh_w
            if "entry" not in fh_w: # this condition can be omitted, check that
                self.write_header()

    def write_header(self):
        """
        Creates and writes the NeXus hierarchy for the dataset,
        including metadata such as instrument configuration, 
        source details, monochromator properties, and integration parameters.

        This method will automatically determine what type(s) of data 
        (1D/2D) are to be written and populate corresponding subentries.
        """
        logging.info(f"writing header started, azint version is {azint.__version__}")

        entry = self.fh.create_group("entry", track_order=True)
        entry.attrs["NX_class"] = "NXentry"
        entry.attrs["default"] = "data" 
        logging.debug("entry is created in the file")


        if not (self.write_1d and self.write_2d):
            logging.info(f"Creating {'NXazint1d' if self.write_1d else 'NXazint2d'}")
            definition = entry.create_dataset("definition", data='NXazint1d' if self.write_1d else 'NXazint2d')       

        solid_angle = entry.create_dataset("solid_angle_applied", data=True if self.ai.solid_angle else False)

        polarization = self.ai.polarization_factor if self.ai.polarization_factor is not None else 0
        polarization_applied = entry.create_dataset("polarization_applied", data=True if self.ai.polarization_factor  is not None else False)

        normalization = entry.create_dataset("normalization_applied", data=True if self.ai.normalized else False)

        logging.info("solid_angle_applied and polarization_applied data sets are created")
        logging.info(f"solid_angle: {self.ai.solid_angle}")
        logging.info(f"polarization_factor: {self.ai.polarization_factor}")


        # Add instrument
        instrument = entry.create_group("instrument", track_order=True)
        instrument.attrs["NX_class"] = "NXinstrument"
        instrument.attrs["default"] = "name" 
        logging.info(f"Instrument: {self.instrument_name}")

        instrument["name"] = np.string_(self.instrument_name)

        # Add monochromator
        mono = instrument.create_group("monochromator", track_order=True)
        mono.attrs["NX_class"] = "NXmonochromator"
        mono.attrs["default"] = "energy"  

        # Add source
        source = instrument.create_group("source", track_order=True)
        source.attrs["NX_class"] = "NXsource"
        source.attrs["default"] = "name" 
        source['name'] = self.source_name
        source['type'] = self.source_type
        source['probe'] = self.source_probe

        poni_file = self.ai.poni
        if isinstance(self.ai.poni, str):
            with open(poni_file, "r") as pf:
                try:
                    logging.info(f"Reading poni file ...")
                    ponif = pf.read()
                    wavelength_found = False
                    for line in ponif.splitlines():
                        if line.startswith("Wavelength:"):
                            wlength_str = line.split(":")[1].strip()
                            try:
                                wlength = float(wlength_str)
                                logging.info(f"From poni file: wavelength: {wlength * 1e10} Å")
                                wavelength_found = True
                            except ValueError as e:
                                logging.error(f"Error converting wavelength to float: {e}")
                                wlength = None
                            break
                    if not wavelength_found:
                        logging.error("Wavelength not found in poni file.")
                        wlength = None
                except Exception as e:
                    logging.error(f"Cannot open poni file: {e}")
        elif isinstance(self.ai.poni, dict):
            ponif = "\n".join(f"{key}: {value}" for key, value in self.ai.poni.items())
            wavelength_found = False
            try:
                wlength = float(self.ai.poni['wavelength'])
                logging.info(f"From poni file: wavelength: {wlength * 1e10} Å")
                wavelength_found = True
            except ValueError as e:
                logging.error(f"Error converting wavelength to float: {e}")
                wlength = None
            if not wavelength_found:
                logging.error("Wavelength not found in poni dict.")
                wlength = None
        else:
            logging.error("Provided format for poni is wrong.")

        # Now handle data splitting
        if (self.write_1d and self.write_2d):
            logging.info(f"Creating 1D and 2D data ...")
            if self.ai.azimuth_axis is None:
                logging.error("2d data is not available.")
            # 1D data subentry
            azint1dSE = entry.create_group("azint1d", track_order=True)
            azint1dSE.attrs["NX_class"] = "NXsubentry"

            definition = azint1dSE.create_dataset("definition", data="NXazint1d")

            azint1dSE["instrument"] = h5py.SoftLink('/entry/instrument')

            reduction = azint1dSE.create_group("reduction", track_order=True)
            reduction.attrs["NX_class"] = "NXprocess"
            prog = reduction.create_dataset("program", data="azint")
            ver = reduction.create_dataset("version", data=f"azint {azint.__version__}\nazint-writer {__version__}")
            date = reduction.create_dataset("date", data=datetime.now().strftime("%A, %B %d, %Y at %I:%M %p"))
            ref = reduction.create_dataset("reference", data="Jensen, A. B., et al., (2022). J. Synchrotron Rad. 29, 1420-1428.\nhttps://doi.org/10.1107/S1600577522008232,\nhttps://maxiv-science.github.io/azint/")
            note = reduction.create_dataset("note", data="Geometry convention:\nAzimuthal origin in the horizontal plane to the right of the beam position, i.e., at 3 o’clock,\non the detector face. Positive azimuthal direction: clockwise.")

            input = reduction.create_group("input", track_order=True)
            input.attrs["NX_class"] = "NXparameters"
            dset = input.create_dataset("poni", data=ponif, track_order=True)
            dset.attrs["filename"] = poni_file if isinstance(self.ai.poni, str) else "Poni dict."

            wavelength = mono.create_dataset("wavelength", data=wlength * 1e10, track_order=True)
            wavelength.attrs["units"] = "angstrom"
            energy = mono.create_dataset("energy", data=(1.2398 * 1e-9) / wlength, track_order=True)
            energy.attrs["units"] = "keV"

            wavelength2 = input.create_dataset("wavelength", data=wlength * 1e10)
            wavelength2.attrs["units"] = "angstrom"

            input.create_dataset("n_splitting", data=self.ai.n_splitting)
            input.create_dataset("radial_bins", data=self.ai.radial_bins)
            input.create_dataset("azimuth_bins", data=1)
            input.create_dataset("unit", data=self.ai.unit)    
            input.create_dataset("mask", data=self.ai.mask_path if self.ai.mask_path else ("A numpy array was provided" if self.ai.mask is not None else "None"))

            input.create_dataset("solid_angle", data=True if self.ai.solid_angle else False)

            polarization = self.ai.polarization_factor if self.ai.polarization_factor is not None else 0
            input.create_dataset("polarization_factor", data=polarization)
            error_model = self.ai.error_model if self.ai.error_model else "None"
            input.create_dataset("error_model", data=error_model)


            azint1d = azint1dSE.create_group("data")
            azint1d.attrs["NX_class"] = "NXdata"
            azint1d.attrs["signal"] = "I"
            azint1d.attrs["axes"] = [".", "radial_axis"]
            azint1d.attrs["interpretation"] = "spectrum"
            self.write_radial_axis(azint1d, self.ai.unit, self.ai.radial_axis, self.ai.radial_bins)

            azint2dSE = entry.create_group("azint2d", track_order=True)
            azint2dSE.attrs["NX_class"] = "NXsubentry"

            definition = azint2dSE.create_dataset("definition", data="NXazint2d")

            azint2dSE["instrument"] = h5py.SoftLink('/entry/instrument')

            reduction = azint2dSE.create_group("reduction", track_order=True)
            reduction.attrs["NX_class"] = "NXprocess"
            prog = reduction.create_dataset("program", data="azint-pipeline")
            ver = reduction.create_dataset("version", data=f"azint {azint.__version__}\nazint-writer {__version__}")
            date = reduction.create_dataset("date", data=datetime.now().strftime("%A, %B %d, %Y at %I:%M %p"))
            ref = reduction.create_dataset("reference", data="Jensen, A. B., et al., (2022). J. Synchrotron Rad. 29, 1420-1428.\nhttps://doi.org/10.1107/S1600577522008232,\nhttps://maxiv-science.github.io/azint/")
            note = reduction.create_dataset("note", data="Geometry convention:\nAzimuthal origin in the horizontal plane to the right of the beam position, i.e., at 3 o’clock,\non the detector face. Positive azimuthal direction: clockwise.")

            input = reduction.create_group("input", track_order=True)
            input.attrs["NX_class"] = "NXparameters"

            wavelength2 = input.create_dataset("wavelength", data=wlength * 1e10)
            wavelength2.attrs["units"] = "angstrom"

            input.create_dataset("n_splitting", data=self.ai.n_splitting)
            input.create_dataset("radial_bins", data=self.ai.radial_bins)
            input.create_dataset("azimuth_bins", data=self.ai.azimuth_bins)
            input.create_dataset("unit", data=self.ai.unit)
            input.create_dataset("mask", data=self.ai.mask_path if self.ai.mask_path else ("A numpy array was provided" if self.ai.mask is not None else "None"))

            input.create_dataset("solid_angle", data=True if self.ai.solid_angle else False)

            polarization = self.ai.polarization_factor if self.ai.polarization_factor is not None else 0
            input.create_dataset("polarization_factor", data=polarization)
            error_model = self.ai.error_model if self.ai.error_model else "None"
            input.create_dataset("error_model", data=error_model)

            azint2d = azint2dSE.create_group("data")
            azint2d.attrs["NX_class"] = "NXdata"
            azint2d.attrs["signal"] = "I"
            azint2d.attrs["axes"] = [".", "azimuthal_axis", "radial_axis"]
            azint2d.attrs["interpretation"] = "image"
            entry["data"] = h5py.SoftLink('/entry/azint1d/data')
            self.write_radial_axis(azint2d, self.ai.unit, self.ai.radial_axis, self.ai.radial_bins)
            dset = azint2d.create_dataset("azimuthal_axis", data=self.ai.azimuth_axis)
            dset.attrs["units"] = "degrees"
            dset.attrs["long_name"] = "azimuthal bin center"

            if isinstance(self.ai.azimuth_bins, Iterable):
                aedges = self.ai.azimuth_bins
            else:
                acentres = self.ai.azimuth_axis
                awidth = acentres[1] - acentres[0]
                aedges = (acentres - 0.5 * awidth)
                aedges = np.append(aedges, aedges[-1] + awidth)

            dset2 = azint2d.create_dataset("azimuthal_axis_edges", data=aedges)
            dset2.attrs["units"] = "degrees"
            dset2.attrs["long_name"] = "azimuthal bin edges"

            azint1dSE.attrs["default"] = "data"
            azint2dSE.attrs["default"] = "data"
            entry.attrs["default"] = "data"

        elif self.write_1d:
            logging.info(f"Creating just 1D data ...")
            # ONLY 1D DATA section
            reduction = entry.create_group("reduction", track_order=True)
            reduction.attrs["NX_class"] = "NXprocess"
            prog = reduction.create_dataset("program", data="azint-pipeline")
            ver = reduction.create_dataset("version", data=f"azint {azint.__version__}\nazint-writer {__version__}")
            date = reduction.create_dataset("date", data=datetime.now().strftime("%A, %B %d, %Y at %I:%M %p"))
            ref = reduction.create_dataset("reference", data="Jensen, A. B., et al., (2022). J. Synchrotron Rad. 29, 1420-1428.\nhttps://doi.org/10.1107/S1600577522008232,\nhttps://maxiv-science.github.io/azint/")
            note = reduction.create_dataset("note", data="Geometry convention:\nAzimuthal origin in the horizontal plane to the right of the beam position, i.e., at 3 o’clock,\non the detector face. Positive azimuthal direction: clockwise.")

            input = reduction.create_group("input", track_order=True)
            input.attrs["NX_class"] = "NXparameters"
            dset = input.create_dataset("poni", data=ponif, track_order=True)
            dset.attrs["filename"] = poni_file
            # Add wavelength and energy to mono
            wavelength = mono.create_dataset("wavelength", data=wlength * 1e10, track_order=True)
            wavelength.attrs["units"] = "angstrom"
            energy = mono.create_dataset("energy", data=(1.2398 * 1e-9) / wlength, track_order=True)
            energy.attrs["units"] = "keV"

            # Add other info from poni to input
            wavelength2 = input.create_dataset("wavelength", data=wlength * 1e10)     
            wavelength2.attrs["units"] = "angstrom"

            input.create_dataset("n_splitting", data=self.ai.n_splitting)
            input.create_dataset("radial_bins", data=self.ai.radial_bins)
            azimuth_bins = self.ai.azimuth_bins if self.ai.azimuth_bins else 1
            input.create_dataset("azimuth_bins", data=azimuth_bins)
            input.create_dataset("unit", data=self.ai.unit)
            input.create_dataset("mask", data=self.ai.mask_path if self.ai.mask_path else ("A numpy array was provided" if self.ai.mask is not None else "None"))

            input.create_dataset("solid_angle", data=True if self.ai.solid_angle else False)

            polarization = self.ai.polarization_factor if self.ai.polarization_factor is not None else 0
            input.create_dataset("polarization_factor", data=polarization)
            error_model = self.ai.error_model if self.ai.error_model else "None"
            input.create_dataset("error_model", data=error_model)

            azint1d = entry.create_group("data", track_order=True)
            azint1d.attrs["NX_class"] = "NXdata"
            azint1d.attrs["signal"] = "I"
            azint1d.attrs["axes"] = [".", "radial_axis"]
            azint1d.attrs["interpretation"] = "spectrum"
            self.write_radial_axis(azint1d, self.ai.unit, self.ai.radial_axis, self.ai.radial_bins)

            entry.attrs["default"] = "data"

        elif self.write_2d:
            logging.info(f"Creating just 2D data ...")
            # ONLY 2D DATA section
            reduction = entry.create_group("reduction", track_order=True)
            reduction.attrs["NX_class"] = "NXprocess"
            prog = reduction.create_dataset("program", data="azint-pipeline")
            ver = reduction.create_dataset("version", data=f"azint {azint.__version__}\nazint-writer {__version__}")
            date = reduction.create_dataset("date", data=datetime.now().strftime("%A, %B %d, %Y at %I:%M %p"))
            ref = reduction.create_dataset("reference", data="Jensen, A. B., et al., (2022). J. Synchrotron Rad. 29, 1420-1428.\nhttps://doi.org/10.1107/S1600577522008232,\nhttps://maxiv-science.github.io/azint/")
            note = reduction.create_dataset("note", data="Geometry convention:\nAzimuthal origin in the horizontal plane to the right of the beam position, i.e., at 3 o’clock,\non the detector face. Positive azimuthal direction: clockwise.")

            input = reduction.create_group("input", track_order=True)
            input.attrs["NX_class"] = "NXparameters"
            dset = input.create_dataset("poni", data=ponif, track_order=True)
            dset.attrs["filename"] = poni_file
            # Add wavelength and energy to mono
            wavelength = mono.create_dataset("wavelength", data=wlength * 1e10, track_order=True)
            wavelength.attrs["units"] = "angstrom"
            energy = mono.create_dataset("energy", data=(1.2398 * 1e-9) / wlength, track_order=True)
            energy.attrs["units"] = "keV"

            # Add other info from poni to input
            wavelength2 = input.create_dataset("wavelength", data=wlength * 1e10)     
            wavelength2.attrs["units"] = "angstrom"

            input.create_dataset("n_splitting", data=self.ai.n_splitting)
            input.create_dataset("radial_bins", data=self.ai.radial_bins)
            azimuth_bins = self.ai.azimuth_bins
            input.create_dataset("azimuth_bins", data=azimuth_bins)
            input.create_dataset("unit", data=self.ai.unit)
            input.create_dataset("mask", data=self.ai.mask_path if self.ai.mask_path else ("A numpy array was provided" if self.ai.mask is not None else "None"))

            input.create_dataset("solid_angle", data=True if self.ai.solid_angle else False)

            polarization = self.ai.polarization_factor if self.ai.polarization_factor is not None else 0
            input.create_dataset("polarization_factor", data=polarization)
            error_model = self.ai.error_model if self.ai.error_model else "None"
            input.create_dataset("error_model", data=error_model)

            azint2d = entry.create_group("data", track_order=True)
            azint2d.attrs["NX_class"] = "NXdata"
            azint2d.attrs["signal"] = "I"
            azint2d.attrs["axes"] = [".", "azimuthal_axis", "radial_axis"]
            azint2d.attrs["interpretation"] = "image"
            self.write_radial_axis(azint2d, self.ai.unit, self.ai.radial_axis, self.ai.radial_bins)
            dset = azint2d.create_dataset("azimuthal_axis", data=self.ai.azimuth_axis)
            dset.attrs["units"] = "degrees"
            dset.attrs["long_name"] = "azimuthal bin center"

            if isinstance(self.ai.azimuth_bins, Iterable):
                aedges = self.ai.azimuth_bins
            else:
                acentres = self.ai.azimuth_axis
                awidth = acentres[1] - acentres[0]
                aedges = (acentres - 0.5 * awidth)
                aedges = np.append(aedges, aedges[-1] + awidth)

            dset2 = azint2d.create_dataset("azimuthal_axis_edges", data=aedges)
            dset2.attrs["units"] = "degrees"
            dset2.attrs["long_name"] = "azimuthal bin edges"

            entry.attrs["default"] = "data"

    def add_data(self, integrated_data):
        """
        Add azimuthal integration data to the HDF5 file under the proper NXdata group.

        Parameters:
        - integrated_data (tuple): Tuple of form (I, errors_1d, cake, errors_2d), where:
            - I: 1D intensity array
            - errors_1d: 1D error array
            - cake: 2D intensity array
            - errors_2d: 2D error array
        """

        I, errors_1d, cake, errors_2d = integrated_data
        data = {}
        if (self.write_1d and self.write_2d):
        # if cake is not None: # will have eta bins
            data["/entry/azint1d/data/I"] = I
            data["/entry/azint2d/data/I"] = cake
            if self.ai.normalized:
                data["/entry/azint2d/data/norm"] = self.ai.norm_2d
                data["/entry/azint1d/data/norm"] = self.ai.norm_1d
            if errors_2d is not None:
                data["/entry/azint2d/data/I_errors"] = errors_2d
            if errors_1d is not None:
                data["/entry/azint1d/data/I_errors"] = errors_1d
        elif self.write_1d:  # must be radial bins only, no eta, ie 1d.
            data["/entry/data/I"] = I
            if self.ai.normalized:
                data["/entry/data/norm"] = self.ai.norm_1d
            if errors_1d is not None:
                data["/entry/data/I_errors"] = errors_1d
        elif self.write_2d:
            data["/entry/data/I"] = cake
            if self.ai.normalized:
                data["/entry/data/norm"] = self.ai.norm_2d
            if errors_2d is not None:
                data["/entry/data/I_errors"] = errors_2d
        else:
            logging.error(f"At least one of 1D or 2D should be written")

        with h5py.File(self.output_file, "r+") as fh_u:
            for key, value in data.items():
                new_dset = fh_u.get(key)
                if not new_dset:
                    new_dset = fh_u.create_dataset(key, dtype=value.dtype,
                                                   shape=(0, *value.shape),
                                                   maxshape=(None, *value.shape),
                                                   chunks=(1, *value.shape))
                    # I and I_error created here
                    new_dset.attrs["units"] = "arbitrary units"
                    new_dset.attrs["long_name"] = "intensity"
                    if "I_error" in key:
                        new_dset.attrs.modify("long_name", "estimated errors on intensity")
                    if "norm" in key:
                        new_dset.attrs.modify("long_name", "number of pixels contributing to the corresponding bin")

                n = new_dset.shape[0]
                new_dset.resize(n + 1, axis=0)
                new_dset[n] = value

    def write_radial_axis(self, group, unit, radial_axis, radial_bins):
        # real dataset for radial axis is always "radial axis"
        dset = group.create_dataset("radial_axis", data=radial_axis, track_order=True)
        dset.attrs["long_name"] = "q" if unit == "q" else "2theta"
        dset.attrs["units"] = "1/angstrom" if unit == "q" else "degrees"

        # Calculate edges
        if isinstance(radial_bins, Iterable):
            edges = radial_bins
        else:
            centres = radial_axis
            width = centres[1]-centres[0]
            edges = (centres-0.5*width)
            edges = np.append(edges,edges[-1]+width)

        dsete = group.create_dataset("radial_axis_edges", data=edges, track_order=True)
        dsete.attrs["long_name"] = "q bin edges" if unit == "q" else "2theta bin edges"
        dsete.attrs["units"] = "1/angstrom" if unit == "q" else "degrees"

add_data(integrated_data)

Add azimuthal integration data to the HDF5 file under the proper NXdata group.

  • integrated_data (tuple): Tuple of form (I, errors_1d, cake, errors_2d), where:
    • I: 1D intensity array
    • errors_1d: 1D error array
    • cake: 2D intensity array
    • errors_2d: 2D error array
Source code in azint_writer/azint_writer.py
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
def add_data(self, integrated_data):
    """
    Add azimuthal integration data to the HDF5 file under the proper NXdata group.

    Parameters:
    - integrated_data (tuple): Tuple of form (I, errors_1d, cake, errors_2d), where:
        - I: 1D intensity array
        - errors_1d: 1D error array
        - cake: 2D intensity array
        - errors_2d: 2D error array
    """

    I, errors_1d, cake, errors_2d = integrated_data
    data = {}
    if (self.write_1d and self.write_2d):
    # if cake is not None: # will have eta bins
        data["/entry/azint1d/data/I"] = I
        data["/entry/azint2d/data/I"] = cake
        if self.ai.normalized:
            data["/entry/azint2d/data/norm"] = self.ai.norm_2d
            data["/entry/azint1d/data/norm"] = self.ai.norm_1d
        if errors_2d is not None:
            data["/entry/azint2d/data/I_errors"] = errors_2d
        if errors_1d is not None:
            data["/entry/azint1d/data/I_errors"] = errors_1d
    elif self.write_1d:  # must be radial bins only, no eta, ie 1d.
        data["/entry/data/I"] = I
        if self.ai.normalized:
            data["/entry/data/norm"] = self.ai.norm_1d
        if errors_1d is not None:
            data["/entry/data/I_errors"] = errors_1d
    elif self.write_2d:
        data["/entry/data/I"] = cake
        if self.ai.normalized:
            data["/entry/data/norm"] = self.ai.norm_2d
        if errors_2d is not None:
            data["/entry/data/I_errors"] = errors_2d
    else:
        logging.error(f"At least one of 1D or 2D should be written")

    with h5py.File(self.output_file, "r+") as fh_u:
        for key, value in data.items():
            new_dset = fh_u.get(key)
            if not new_dset:
                new_dset = fh_u.create_dataset(key, dtype=value.dtype,
                                               shape=(0, *value.shape),
                                               maxshape=(None, *value.shape),
                                               chunks=(1, *value.shape))
                # I and I_error created here
                new_dset.attrs["units"] = "arbitrary units"
                new_dset.attrs["long_name"] = "intensity"
                if "I_error" in key:
                    new_dset.attrs.modify("long_name", "estimated errors on intensity")
                if "norm" in key:
                    new_dset.attrs.modify("long_name", "number of pixels contributing to the corresponding bin")

            n = new_dset.shape[0]
            new_dset.resize(n + 1, axis=0)
            new_dset[n] = value

write_header()

Creates and writes the NeXus hierarchy for the dataset, including metadata such as instrument configuration, source details, monochromator properties, and integration parameters.

This method will automatically determine what type(s) of data (1D/2D) are to be written and populate corresponding subentries.

Source code in azint_writer/azint_writer.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
def write_header(self):
    """
    Creates and writes the NeXus hierarchy for the dataset,
    including metadata such as instrument configuration, 
    source details, monochromator properties, and integration parameters.

    This method will automatically determine what type(s) of data 
    (1D/2D) are to be written and populate corresponding subentries.
    """
    logging.info(f"writing header started, azint version is {azint.__version__}")

    entry = self.fh.create_group("entry", track_order=True)
    entry.attrs["NX_class"] = "NXentry"
    entry.attrs["default"] = "data" 
    logging.debug("entry is created in the file")


    if not (self.write_1d and self.write_2d):
        logging.info(f"Creating {'NXazint1d' if self.write_1d else 'NXazint2d'}")
        definition = entry.create_dataset("definition", data='NXazint1d' if self.write_1d else 'NXazint2d')       

    solid_angle = entry.create_dataset("solid_angle_applied", data=True if self.ai.solid_angle else False)

    polarization = self.ai.polarization_factor if self.ai.polarization_factor is not None else 0
    polarization_applied = entry.create_dataset("polarization_applied", data=True if self.ai.polarization_factor  is not None else False)

    normalization = entry.create_dataset("normalization_applied", data=True if self.ai.normalized else False)

    logging.info("solid_angle_applied and polarization_applied data sets are created")
    logging.info(f"solid_angle: {self.ai.solid_angle}")
    logging.info(f"polarization_factor: {self.ai.polarization_factor}")


    # Add instrument
    instrument = entry.create_group("instrument", track_order=True)
    instrument.attrs["NX_class"] = "NXinstrument"
    instrument.attrs["default"] = "name" 
    logging.info(f"Instrument: {self.instrument_name}")

    instrument["name"] = np.string_(self.instrument_name)

    # Add monochromator
    mono = instrument.create_group("monochromator", track_order=True)
    mono.attrs["NX_class"] = "NXmonochromator"
    mono.attrs["default"] = "energy"  

    # Add source
    source = instrument.create_group("source", track_order=True)
    source.attrs["NX_class"] = "NXsource"
    source.attrs["default"] = "name" 
    source['name'] = self.source_name
    source['type'] = self.source_type
    source['probe'] = self.source_probe

    poni_file = self.ai.poni
    if isinstance(self.ai.poni, str):
        with open(poni_file, "r") as pf:
            try:
                logging.info(f"Reading poni file ...")
                ponif = pf.read()
                wavelength_found = False
                for line in ponif.splitlines():
                    if line.startswith("Wavelength:"):
                        wlength_str = line.split(":")[1].strip()
                        try:
                            wlength = float(wlength_str)
                            logging.info(f"From poni file: wavelength: {wlength * 1e10} Å")
                            wavelength_found = True
                        except ValueError as e:
                            logging.error(f"Error converting wavelength to float: {e}")
                            wlength = None
                        break
                if not wavelength_found:
                    logging.error("Wavelength not found in poni file.")
                    wlength = None
            except Exception as e:
                logging.error(f"Cannot open poni file: {e}")
    elif isinstance(self.ai.poni, dict):
        ponif = "\n".join(f"{key}: {value}" for key, value in self.ai.poni.items())
        wavelength_found = False
        try:
            wlength = float(self.ai.poni['wavelength'])
            logging.info(f"From poni file: wavelength: {wlength * 1e10} Å")
            wavelength_found = True
        except ValueError as e:
            logging.error(f"Error converting wavelength to float: {e}")
            wlength = None
        if not wavelength_found:
            logging.error("Wavelength not found in poni dict.")
            wlength = None
    else:
        logging.error("Provided format for poni is wrong.")

    # Now handle data splitting
    if (self.write_1d and self.write_2d):
        logging.info(f"Creating 1D and 2D data ...")
        if self.ai.azimuth_axis is None:
            logging.error("2d data is not available.")
        # 1D data subentry
        azint1dSE = entry.create_group("azint1d", track_order=True)
        azint1dSE.attrs["NX_class"] = "NXsubentry"

        definition = azint1dSE.create_dataset("definition", data="NXazint1d")

        azint1dSE["instrument"] = h5py.SoftLink('/entry/instrument')

        reduction = azint1dSE.create_group("reduction", track_order=True)
        reduction.attrs["NX_class"] = "NXprocess"
        prog = reduction.create_dataset("program", data="azint")
        ver = reduction.create_dataset("version", data=f"azint {azint.__version__}\nazint-writer {__version__}")
        date = reduction.create_dataset("date", data=datetime.now().strftime("%A, %B %d, %Y at %I:%M %p"))
        ref = reduction.create_dataset("reference", data="Jensen, A. B., et al., (2022). J. Synchrotron Rad. 29, 1420-1428.\nhttps://doi.org/10.1107/S1600577522008232,\nhttps://maxiv-science.github.io/azint/")
        note = reduction.create_dataset("note", data="Geometry convention:\nAzimuthal origin in the horizontal plane to the right of the beam position, i.e., at 3 o’clock,\non the detector face. Positive azimuthal direction: clockwise.")

        input = reduction.create_group("input", track_order=True)
        input.attrs["NX_class"] = "NXparameters"
        dset = input.create_dataset("poni", data=ponif, track_order=True)
        dset.attrs["filename"] = poni_file if isinstance(self.ai.poni, str) else "Poni dict."

        wavelength = mono.create_dataset("wavelength", data=wlength * 1e10, track_order=True)
        wavelength.attrs["units"] = "angstrom"
        energy = mono.create_dataset("energy", data=(1.2398 * 1e-9) / wlength, track_order=True)
        energy.attrs["units"] = "keV"

        wavelength2 = input.create_dataset("wavelength", data=wlength * 1e10)
        wavelength2.attrs["units"] = "angstrom"

        input.create_dataset("n_splitting", data=self.ai.n_splitting)
        input.create_dataset("radial_bins", data=self.ai.radial_bins)
        input.create_dataset("azimuth_bins", data=1)
        input.create_dataset("unit", data=self.ai.unit)    
        input.create_dataset("mask", data=self.ai.mask_path if self.ai.mask_path else ("A numpy array was provided" if self.ai.mask is not None else "None"))

        input.create_dataset("solid_angle", data=True if self.ai.solid_angle else False)

        polarization = self.ai.polarization_factor if self.ai.polarization_factor is not None else 0
        input.create_dataset("polarization_factor", data=polarization)
        error_model = self.ai.error_model if self.ai.error_model else "None"
        input.create_dataset("error_model", data=error_model)


        azint1d = azint1dSE.create_group("data")
        azint1d.attrs["NX_class"] = "NXdata"
        azint1d.attrs["signal"] = "I"
        azint1d.attrs["axes"] = [".", "radial_axis"]
        azint1d.attrs["interpretation"] = "spectrum"
        self.write_radial_axis(azint1d, self.ai.unit, self.ai.radial_axis, self.ai.radial_bins)

        azint2dSE = entry.create_group("azint2d", track_order=True)
        azint2dSE.attrs["NX_class"] = "NXsubentry"

        definition = azint2dSE.create_dataset("definition", data="NXazint2d")

        azint2dSE["instrument"] = h5py.SoftLink('/entry/instrument')

        reduction = azint2dSE.create_group("reduction", track_order=True)
        reduction.attrs["NX_class"] = "NXprocess"
        prog = reduction.create_dataset("program", data="azint-pipeline")
        ver = reduction.create_dataset("version", data=f"azint {azint.__version__}\nazint-writer {__version__}")
        date = reduction.create_dataset("date", data=datetime.now().strftime("%A, %B %d, %Y at %I:%M %p"))
        ref = reduction.create_dataset("reference", data="Jensen, A. B., et al., (2022). J. Synchrotron Rad. 29, 1420-1428.\nhttps://doi.org/10.1107/S1600577522008232,\nhttps://maxiv-science.github.io/azint/")
        note = reduction.create_dataset("note", data="Geometry convention:\nAzimuthal origin in the horizontal plane to the right of the beam position, i.e., at 3 o’clock,\non the detector face. Positive azimuthal direction: clockwise.")

        input = reduction.create_group("input", track_order=True)
        input.attrs["NX_class"] = "NXparameters"

        wavelength2 = input.create_dataset("wavelength", data=wlength * 1e10)
        wavelength2.attrs["units"] = "angstrom"

        input.create_dataset("n_splitting", data=self.ai.n_splitting)
        input.create_dataset("radial_bins", data=self.ai.radial_bins)
        input.create_dataset("azimuth_bins", data=self.ai.azimuth_bins)
        input.create_dataset("unit", data=self.ai.unit)
        input.create_dataset("mask", data=self.ai.mask_path if self.ai.mask_path else ("A numpy array was provided" if self.ai.mask is not None else "None"))

        input.create_dataset("solid_angle", data=True if self.ai.solid_angle else False)

        polarization = self.ai.polarization_factor if self.ai.polarization_factor is not None else 0
        input.create_dataset("polarization_factor", data=polarization)
        error_model = self.ai.error_model if self.ai.error_model else "None"
        input.create_dataset("error_model", data=error_model)

        azint2d = azint2dSE.create_group("data")
        azint2d.attrs["NX_class"] = "NXdata"
        azint2d.attrs["signal"] = "I"
        azint2d.attrs["axes"] = [".", "azimuthal_axis", "radial_axis"]
        azint2d.attrs["interpretation"] = "image"
        entry["data"] = h5py.SoftLink('/entry/azint1d/data')
        self.write_radial_axis(azint2d, self.ai.unit, self.ai.radial_axis, self.ai.radial_bins)
        dset = azint2d.create_dataset("azimuthal_axis", data=self.ai.azimuth_axis)
        dset.attrs["units"] = "degrees"
        dset.attrs["long_name"] = "azimuthal bin center"

        if isinstance(self.ai.azimuth_bins, Iterable):
            aedges = self.ai.azimuth_bins
        else:
            acentres = self.ai.azimuth_axis
            awidth = acentres[1] - acentres[0]
            aedges = (acentres - 0.5 * awidth)
            aedges = np.append(aedges, aedges[-1] + awidth)

        dset2 = azint2d.create_dataset("azimuthal_axis_edges", data=aedges)
        dset2.attrs["units"] = "degrees"
        dset2.attrs["long_name"] = "azimuthal bin edges"

        azint1dSE.attrs["default"] = "data"
        azint2dSE.attrs["default"] = "data"
        entry.attrs["default"] = "data"

    elif self.write_1d:
        logging.info(f"Creating just 1D data ...")
        # ONLY 1D DATA section
        reduction = entry.create_group("reduction", track_order=True)
        reduction.attrs["NX_class"] = "NXprocess"
        prog = reduction.create_dataset("program", data="azint-pipeline")
        ver = reduction.create_dataset("version", data=f"azint {azint.__version__}\nazint-writer {__version__}")
        date = reduction.create_dataset("date", data=datetime.now().strftime("%A, %B %d, %Y at %I:%M %p"))
        ref = reduction.create_dataset("reference", data="Jensen, A. B., et al., (2022). J. Synchrotron Rad. 29, 1420-1428.\nhttps://doi.org/10.1107/S1600577522008232,\nhttps://maxiv-science.github.io/azint/")
        note = reduction.create_dataset("note", data="Geometry convention:\nAzimuthal origin in the horizontal plane to the right of the beam position, i.e., at 3 o’clock,\non the detector face. Positive azimuthal direction: clockwise.")

        input = reduction.create_group("input", track_order=True)
        input.attrs["NX_class"] = "NXparameters"
        dset = input.create_dataset("poni", data=ponif, track_order=True)
        dset.attrs["filename"] = poni_file
        # Add wavelength and energy to mono
        wavelength = mono.create_dataset("wavelength", data=wlength * 1e10, track_order=True)
        wavelength.attrs["units"] = "angstrom"
        energy = mono.create_dataset("energy", data=(1.2398 * 1e-9) / wlength, track_order=True)
        energy.attrs["units"] = "keV"

        # Add other info from poni to input
        wavelength2 = input.create_dataset("wavelength", data=wlength * 1e10)     
        wavelength2.attrs["units"] = "angstrom"

        input.create_dataset("n_splitting", data=self.ai.n_splitting)
        input.create_dataset("radial_bins", data=self.ai.radial_bins)
        azimuth_bins = self.ai.azimuth_bins if self.ai.azimuth_bins else 1
        input.create_dataset("azimuth_bins", data=azimuth_bins)
        input.create_dataset("unit", data=self.ai.unit)
        input.create_dataset("mask", data=self.ai.mask_path if self.ai.mask_path else ("A numpy array was provided" if self.ai.mask is not None else "None"))

        input.create_dataset("solid_angle", data=True if self.ai.solid_angle else False)

        polarization = self.ai.polarization_factor if self.ai.polarization_factor is not None else 0
        input.create_dataset("polarization_factor", data=polarization)
        error_model = self.ai.error_model if self.ai.error_model else "None"
        input.create_dataset("error_model", data=error_model)

        azint1d = entry.create_group("data", track_order=True)
        azint1d.attrs["NX_class"] = "NXdata"
        azint1d.attrs["signal"] = "I"
        azint1d.attrs["axes"] = [".", "radial_axis"]
        azint1d.attrs["interpretation"] = "spectrum"
        self.write_radial_axis(azint1d, self.ai.unit, self.ai.radial_axis, self.ai.radial_bins)

        entry.attrs["default"] = "data"

    elif self.write_2d:
        logging.info(f"Creating just 2D data ...")
        # ONLY 2D DATA section
        reduction = entry.create_group("reduction", track_order=True)
        reduction.attrs["NX_class"] = "NXprocess"
        prog = reduction.create_dataset("program", data="azint-pipeline")
        ver = reduction.create_dataset("version", data=f"azint {azint.__version__}\nazint-writer {__version__}")
        date = reduction.create_dataset("date", data=datetime.now().strftime("%A, %B %d, %Y at %I:%M %p"))
        ref = reduction.create_dataset("reference", data="Jensen, A. B., et al., (2022). J. Synchrotron Rad. 29, 1420-1428.\nhttps://doi.org/10.1107/S1600577522008232,\nhttps://maxiv-science.github.io/azint/")
        note = reduction.create_dataset("note", data="Geometry convention:\nAzimuthal origin in the horizontal plane to the right of the beam position, i.e., at 3 o’clock,\non the detector face. Positive azimuthal direction: clockwise.")

        input = reduction.create_group("input", track_order=True)
        input.attrs["NX_class"] = "NXparameters"
        dset = input.create_dataset("poni", data=ponif, track_order=True)
        dset.attrs["filename"] = poni_file
        # Add wavelength and energy to mono
        wavelength = mono.create_dataset("wavelength", data=wlength * 1e10, track_order=True)
        wavelength.attrs["units"] = "angstrom"
        energy = mono.create_dataset("energy", data=(1.2398 * 1e-9) / wlength, track_order=True)
        energy.attrs["units"] = "keV"

        # Add other info from poni to input
        wavelength2 = input.create_dataset("wavelength", data=wlength * 1e10)     
        wavelength2.attrs["units"] = "angstrom"

        input.create_dataset("n_splitting", data=self.ai.n_splitting)
        input.create_dataset("radial_bins", data=self.ai.radial_bins)
        azimuth_bins = self.ai.azimuth_bins
        input.create_dataset("azimuth_bins", data=azimuth_bins)
        input.create_dataset("unit", data=self.ai.unit)
        input.create_dataset("mask", data=self.ai.mask_path if self.ai.mask_path else ("A numpy array was provided" if self.ai.mask is not None else "None"))

        input.create_dataset("solid_angle", data=True if self.ai.solid_angle else False)

        polarization = self.ai.polarization_factor if self.ai.polarization_factor is not None else 0
        input.create_dataset("polarization_factor", data=polarization)
        error_model = self.ai.error_model if self.ai.error_model else "None"
        input.create_dataset("error_model", data=error_model)

        azint2d = entry.create_group("data", track_order=True)
        azint2d.attrs["NX_class"] = "NXdata"
        azint2d.attrs["signal"] = "I"
        azint2d.attrs["axes"] = [".", "azimuthal_axis", "radial_axis"]
        azint2d.attrs["interpretation"] = "image"
        self.write_radial_axis(azint2d, self.ai.unit, self.ai.radial_axis, self.ai.radial_bins)
        dset = azint2d.create_dataset("azimuthal_axis", data=self.ai.azimuth_axis)
        dset.attrs["units"] = "degrees"
        dset.attrs["long_name"] = "azimuthal bin center"

        if isinstance(self.ai.azimuth_bins, Iterable):
            aedges = self.ai.azimuth_bins
        else:
            acentres = self.ai.azimuth_axis
            awidth = acentres[1] - acentres[0]
            aedges = (acentres - 0.5 * awidth)
            aedges = np.append(aedges, aedges[-1] + awidth)

        dset2 = azint2d.create_dataset("azimuthal_axis_edges", data=aedges)
        dset2.attrs["units"] = "degrees"
        dset2.attrs["long_name"] = "azimuthal bin edges"

        entry.attrs["default"] = "data"