brimfile.data

   1import numpy as np
   2import asyncio
   3
   4import warnings
   5from enum import Enum
   6
   7from .file_abstraction import FileAbstraction, sync, _async_getitem, _gather_sync
   8from .utils import concatenate_paths, list_objects_matching_pattern, get_object_name, set_object_name
   9from .utils import var_to_singleton, np_array_to_smallest_int_type, _guess_chunks
  10
  11from .metadata import Metadata
  12
  13from numbers import Number
  14
  15from . import units
  16from .constants import brim_obj_names
  17
  18__docformat__ = "google"
  19
  20
  21class Data:
  22    """
  23    Represents a data group within the brim file.
  24    """
  25
  26    def __init__(self, file: FileAbstraction, path: str):
  27        """
  28        Initialize the Data object. This constructor should not be called directly.
  29
  30        Args:
  31            file (File): The parent File object.
  32            path (str): The path to the data group within the file.
  33        """
  34        self._file = file
  35        self._path = path
  36        self._group = sync(file.open_group(path))
  37
  38        self._spatial_map, self._spatial_map_px_size = self._load_spatial_mapping()
  39
  40    def get_name(self):
  41        """
  42        Returns the name of the data group.
  43        """
  44        return sync(get_object_name(self._file, self._path))
  45    
  46    def get_index(self):
  47        """
  48        Returns the index of the data group.
  49        """
  50        return int(self._path.split('/')[-1].split('_')[-1])
  51
  52    def _load_spatial_mapping(self, load_in_memory: bool=True) -> tuple:
  53        """
  54        Load a spatial mapping in the same format as 'Cartesian visualisation',
  55        irrespectively on whether 'Spatial_map' is defined instead.
  56        -1 is used for "empty" pixels in the image
  57        Args:
  58            load_in_memory (bool): Specify whether the map should be forced to load in memory or just opened as a dataset.
  59        Returns:
  60            The spatial map and the corresponding pixel size as a tuple of 3 Metadata.Item, both in the order z, y, x.
  61        """
  62        cv = None
  63        px_size = 3*(Metadata.Item(value=1, units=None),)
  64
  65        cv_path = concatenate_paths(
  66            self._path, brim_obj_names.data.cartesian_visualisation)
  67        sm_path = concatenate_paths(
  68            self._path, brim_obj_names.data.spatial_map)
  69        
  70        if sync(self._file.object_exists(cv_path)):
  71            cv = sync(self._file.open_dataset(cv_path))
  72
  73            #read the pixel size from the 'Cartesian visualisation' dataset
  74            px_size_val = None
  75            px_size_units = None
  76            try:
  77                px_size_val = sync(self._file.get_attr(cv, 'element_size'))
  78                if px_size_val is None or len(px_size_val) != 3:
  79                    raise ValueError(
  80                        "The 'element_size' attribute of 'Cartesian_visualisation' must be a tuple of 3 elements")
  81            except Exception:
  82                px_size_val = 3*(1,)
  83                warnings.warn(
  84                    "No pixel size defined for Cartesian visualisation")            
  85            px_size_units = sync(units.of_attribute(
  86                    self._file, cv, 'element_size'))
  87            px_size = ()
  88            for i in range(3):
  89                # if px_size_val[i] is not a number, set it to 1 and px_size_units to None
  90                if isinstance(px_size_val[i], Number):
  91                    px_size += (Metadata.Item(px_size_val[i], px_size_units), )
  92                else:
  93                    px_size += (Metadata.Item(1, None), )
  94                    
  95
  96            if load_in_memory:
  97                cv = np.array(cv)
  98                cv = np_array_to_smallest_int_type(cv)
  99
 100        elif sync(self._file.object_exists(sm_path)):
 101            def load_spatial_map_from_file(self):
 102                async def load_coordinate_from_sm(coord: str):
 103                    res = np.empty(0)  # empty array
 104                    try:
 105                        res = await self._file.open_dataset(
 106                            concatenate_paths(sm_path, coord))
 107                        res = await res.to_np_array()
 108                        res = np.squeeze(res)  # remove single-dimensional entries
 109                    except Exception as e:
 110                        # if the coordinate does not exist, return an empty array
 111                        pass
 112                    if len(res.shape) > 1:
 113                        raise ValueError(
 114                            f"The 'Spatial_map/{coord}' dataset is not a 1D array as expected")
 115                    return res
 116
 117                def check_coord_array(arr, size):
 118                    if arr.size == 0:
 119                        return np.zeros(size)
 120                    elif arr.size != size:
 121                        raise ValueError(
 122                            "The 'Spatial_map' dataset is invalid")
 123                    return arr
 124
 125                x, y, z = _gather_sync(
 126                    load_coordinate_from_sm('x'),
 127                    load_coordinate_from_sm('y'),
 128                    load_coordinate_from_sm('z')
 129                    )
 130                size = max([x.size, y.size, z.size])
 131                if size == 0:
 132                    raise ValueError("The 'Spatial_map' dataset is empty")
 133                x = check_coord_array(x, size)
 134                y = check_coord_array(y, size)
 135                z = check_coord_array(z, size)
 136                return x, y, z
 137
 138            def calculate_step(x):
 139                n = len(np.unique(x))
 140                if n == 1:
 141                    d = None
 142                else:
 143                    d = (np.max(x)-np.min(x))/(n-1)
 144                return n, d
 145
 146            x, y, z = load_spatial_map_from_file(self)
 147
 148            # TODO extend the reconstruction to non-cartesian cases
 149
 150            nX, dX = calculate_step(x)
 151            nY, dY = calculate_step(y)
 152            nZ, dZ = calculate_step(z)
 153
 154            indices = np_array_to_smallest_int_type(np.lexsort((x, y, z)))
 155            cv = np.reshape(indices, (nZ, nY, nX))
 156
 157            px_size_units = sync(units.of_object(self._file, sm_path))
 158            px_size = ()
 159            for i in range(3):
 160                px_sz = (dZ, dY, dX)[i]
 161                px_unit = px_size_units
 162                if px_sz is None:
 163                    px_sz = 1
 164                    px_unit = None
 165                px_size += (Metadata.Item(px_sz, px_unit),)
 166
 167        return cv, px_size
 168
 169    def get_PSD(self) -> tuple:
 170        """
 171        LOW LEVEL FUNCTION
 172
 173        Retrieve the Power Spectral Density (PSD) and frequency from the current data group.
 174        Note: this function exposes the internals of the brim file and thus the interface might change in future versions.
 175        Use only if more specialized functions are not working for your application!
 176        Returns:
 177            tuple: (PSD, frequency, PSD_units, frequency_units)
 178                - PSD: A 2D (or more) numpy array containing all the spectra (see [specs](https://github.com/prevedel-lab/Brillouin-standard-file/blob/main/docs/brim_file_specs.md) for more details).
 179                - frequency: A numpy array representing the frequency data (see [specs](https://github.com/prevedel-lab/Brillouin-standard-file/blob/main/docs/brim_file_specs.md) for more details).
 180                - PSD_units: The units of the PSD.
 181                - frequency_units: The units of the frequency.
 182        """
 183        PSD, frequency = _gather_sync(
 184            self._file.open_dataset(concatenate_paths(
 185                self._path, brim_obj_names.data.PSD)),
 186            self._file.open_dataset(concatenate_paths(
 187                self._path, brim_obj_names.data.frequency))
 188        )
 189        # retrieve the units of the PSD and frequency
 190        PSD_units, frequency_units = _gather_sync(
 191            units.of_object(self._file, PSD),
 192            units.of_object(self._file, frequency)
 193        )
 194
 195        return PSD, frequency, PSD_units, frequency_units
 196    
 197    def get_PSD_as_spatial_map(self) -> tuple:
 198        """
 199        Retrieve the Power Spectral Density (PSD) as a spatial map and the frequency from the current data group.
 200        Returns:
 201            tuple: (PSD, frequency, PSD_units, frequency_units)
 202                - PSD: A 4D (or more) numpy array containing all the spectra. Dimensions are z, y, x, [parameters], spectrum.
 203                - frequency: A numpy array representing the frequency data, which can be broadcastable to PSD.
 204                - PSD_units: The units of the PSD.
 205                - frequency_units: The units of the frequency.
 206        """
 207        PSD, frequency = _gather_sync(
 208            self._file.open_dataset(concatenate_paths(
 209                self._path, brim_obj_names.data.PSD)),        
 210            self._file.open_dataset(concatenate_paths(
 211                self._path, brim_obj_names.data.frequency))
 212            )        
 213        # retrieve the units of the PSD and frequency
 214        PSD_units, frequency_units = _gather_sync(
 215            units.of_object(self._file, PSD),
 216            units.of_object(self._file, frequency)
 217        )
 218
 219        # ensure PSD and frequency are numpy arrays
 220        PSD = np.array(PSD)  
 221        frequency = np.array(frequency)  # ensure it's a numpy array
 222        
 223        #if the frequency is not the same for all spectra, broadcast it to match the shape of PSD
 224        if frequency.ndim > 1:
 225            frequency = np.broadcast_to(frequency, PSD.shape)
 226        
 227        sm = np.array(self._spatial_map)
 228        # reshape the PSD to have the spatial dimensions first      
 229        PSD = PSD[sm, ...]
 230        # reshape the frequency pnly if it is not the same for all spectra
 231        if frequency.ndim > 1:
 232            frequency = frequency[sm, ...]
 233
 234        return PSD, frequency, PSD_units, frequency_units
 235
 236    def get_spectrum(self, index: int) -> tuple:
 237        """
 238        Synchronous wrapper for `get_spectrum_async` (see doc for `brimfile.data.Data.get_spectrum_async`)
 239        """
 240        return sync(self.get_spectrum_async(index))
 241    async def get_spectrum_async(self, index: int) -> tuple:
 242        """
 243        Retrieve a spectrum from the data group.
 244
 245        Args:
 246            index (int): The index of the spectrum to retrieve.
 247
 248        Returns:
 249            tuple: (PSD, frequency, PSD_units, frequency_units) for the specified index. 
 250                    PSD can be 1D or more (if there are additional parameters);
 251                    frequency has the same size as PSD
 252        Raises:
 253            IndexError: If the index is out of range for the PSD dataset.
 254        """
 255        # index = -1 corresponds to no spectrum
 256        if index < 0:
 257            return None, None, None, None
 258        PSD, frequency = await asyncio.gather(
 259            self._file.open_dataset(concatenate_paths(
 260                self._path, brim_obj_names.data.PSD)),                       
 261            self._file.open_dataset(concatenate_paths(
 262                self._path, brim_obj_names.data.frequency))
 263            )
 264        if index >= PSD.shape[0]:
 265            raise IndexError(
 266                f"index {index} out of range for PSD with shape {PSD.shape}") 
 267        # retrieve the units of the PSD and frequency
 268        PSD_units, frequency_units = await asyncio.gather(
 269            units.of_object(self._file, PSD),
 270            units.of_object(self._file, frequency)
 271        )
 272        # map index to the frequency array, considering the broadcasting rules
 273        index_frequency = (index, ...)
 274        if frequency.ndim < PSD.ndim:
 275            # given the definition of the brim file format,
 276            # if the frequency has less dimensions that PSD,
 277            # it can only be because it is the same for all the spatial position (first dimension)
 278            index_frequency = (..., )
 279        #get the spectrum and the corresponding frequency at the specified index
 280        PSD, frequency = await asyncio.gather(
 281            _async_getitem(PSD, (index,...)),
 282            _async_getitem(frequency, index_frequency)
 283        )
 284        #broadcast the frequency to match the shape of PSD if needed
 285        if frequency.ndim < PSD.ndim:
 286            frequency = np.broadcast_to(frequency, PSD.shape)
 287        return PSD, frequency, PSD_units, frequency_units
 288
 289    def get_spectrum_in_image(self, coor: tuple) -> tuple:
 290        """
 291        Retrieve a spectrum from the data group using spatial coordinates.
 292
 293        Args:
 294            coor (tuple): A tuple containing the z, y, x coordinates of the spectrum to retrieve.
 295
 296        Returns:
 297            tuple: A tuple containing the PSD, frequency, PSD_units, frequency_units for the specified coordinates. See "get_spectrum" for details.
 298        """
 299        if len(coor) != 3:
 300            raise ValueError("coor must contain 3 values for z, y, x")
 301
 302        index = int(self._spatial_map[coor])
 303        return self.get_spectrum(index)    
 304          
 305    def get_spectrum_and_all_quantities_in_image(self, ar: 'Data.AnalysisResults', coor: tuple, index_peak: int = 0):
 306        """
 307            Retrieve the spectrum and all available quantities from the analysis results at a specific spatial coordinate.
 308            TODO complete the documentation
 309        """
 310        if len(coor) != 3:
 311            raise ValueError("coor must contain 3 values for z, y, x")
 312        index = int(self._spatial_map[coor])
 313        spectrum, quantities = _gather_sync(
 314            self.get_spectrum_async(index),
 315            ar._get_all_quantities_at_index(index, index_peak)
 316        )
 317        return spectrum, quantities
 318
 319    class AnalysisResults:
 320        """
 321        Rapresents the analysis results associated with a Data object.
 322        """
 323
 324        class Quantity(Enum):
 325            """
 326            Enum representing the type of analysis results.
 327            """
 328            Shift = "Shift"
 329            Width = "Width"
 330            Amplitude = "Amplitude"
 331            Offset = "Offset"
 332            R2 = "R2"
 333            RMSE = "RMSE"
 334            Cov_matrix = "Cov_matrix"
 335
 336        class PeakType(Enum):
 337            AntiStokes = "AS"
 338            Stokes = "S"
 339            average = "avg"
 340        
 341        class FitModel(Enum):
 342            Undefined = "Undefined"
 343            Lorentzian = "Lorentzian"
 344            DHO = "DHO"
 345            Gaussian = "Gaussian"
 346            Voigt = "Voigt"
 347            Custom = "Custom"
 348
 349        def __init__(self, file: FileAbstraction, full_path: str, spatial_map, spatial_map_px_size):
 350            """
 351            Initialize the AnalysisResults object.
 352
 353            Args:
 354                file (File): The parent File object.
 355                full_path (str): path of the group storing the analysis results
 356            """
 357            self._file = file
 358            self._path = full_path
 359            # self._group = file.open_group(full_path)
 360            self._spatial_map = spatial_map
 361            self._spatial_map_px_size = spatial_map_px_size
 362
 363        def get_name(self):
 364            """
 365            Returns the name of the Analysis group.
 366            """
 367            return sync(get_object_name(self._file, self._path))
 368
 369        @classmethod
 370        def _create_new(cls, data: 'Data', index: int) -> 'Data.AnalysisResults':
 371            """
 372            Create a new AnalysisResults group.
 373
 374            Args:
 375                file (FileAbstraction): The file.
 376                index (int): The index for the new AnalysisResults group.
 377
 378            Returns:
 379                AnalysisResults: The newly created AnalysisResults object.
 380            """
 381            group_name = f"{brim_obj_names.data.analysis_results}_{index}"
 382            ar_full_path = concatenate_paths(data._path, group_name)
 383            group = sync(data._file.create_group(ar_full_path))
 384            return cls(data._file, ar_full_path, data._spatial_map, data._spatial_map_px_size)
 385
 386        def add_data(self, data_AntiStokes=None, data_Stokes=None, fit_model: 'Data.AnalysisResults.FitModel' = None):
 387            """
 388            Adds data for the analysis results for AntiStokes and Stokes peaks to the file.
 389            
 390            Args:
 391                data_AntiStokes (dict or list[dict]): A dictionary containing the analysis results for AntiStokes peaks.
 392                    In case multiple peaks were fitted, it might be a list of dictionaries with each element corresponding to a single peak.
 393                
 394                    Each dictionary may include the following keys (plus the corresponding units,  e.g. 'shift_units'):
 395                        - 'shift': The shift value.
 396                        - 'width': The width value.
 397                        - 'amplitude': The amplitude value.
 398                        - 'offset': The offset value.
 399                        - 'R2': The R-squared value.
 400                        - 'RMSE': The root mean square error value.
 401                        - 'Cov_matrix': The covariance matrix.
 402                data_Stokes (dict or list[dict]): same as `data_AntiStokes` for the Stokes peaks.
 403                fit_model (Data.AnalysisResults.FitModel, optional): The fit model used for the analysis. Defaults to None (no attribute is set).
 404
 405                Both `data_AntiStokes` and `data_Stokes` are optional, but at least one of them must be provided.
 406            """
 407
 408            ar_cls = Data.AnalysisResults
 409            ar_group = sync(self._file.open_group(self._path))
 410
 411            def add_quantity(qt: Data.AnalysisResults.Quantity, pt: Data.AnalysisResults.PeakType, data, index: int = 0):
 412                # TODO: check if the data is valid
 413                sync(self._file.create_dataset(
 414                    ar_group, ar_cls._get_quantity_name(qt, pt, index), data))
 415
 416            def add_data_pt(pt: Data.AnalysisResults.PeakType, data, index: int = 0):
 417                if 'shift' in data:
 418                    add_quantity(ar_cls.Quantity.Shift,
 419                                 pt, data['shift'], index)
 420                    if 'shift_units' in data:
 421                        self._set_units(data['shift_units'],
 422                                        ar_cls.Quantity.Shift, pt, index)
 423                if 'width' in data:
 424                    add_quantity(ar_cls.Quantity.Width,
 425                                 pt, data['width'], index)
 426                    if 'width_units' in data:
 427                        self._set_units(data['width_units'],
 428                                        ar_cls.Quantity.Width, pt, index)
 429                if 'amplitude' in data:
 430                    add_quantity(ar_cls.Quantity.Amplitude,
 431                                 pt, data['amplitude'], index)
 432                    if 'amplitude_units' in data:
 433                        self._set_units(
 434                            data['amplitude_units'], ar_cls.Quantity.Amplitude, pt, index)
 435                if 'offset' in data:
 436                    add_quantity(ar_cls.Quantity.Offset,
 437                                 pt, data['offset'], index)
 438                    if 'offset_units' in data:
 439                        self._set_units(
 440                            data['offset_units'], ar_cls.Quantity.Offset, pt, index)
 441                if 'R2' in data:
 442                    add_quantity(ar_cls.Quantity.R2, pt, data['R2'], index)
 443                    if 'R2_units' in data:
 444                        self._set_units(data['R2_units'],
 445                                        ar_cls.Quantity.R2, pt, index)
 446                if 'RMSE' in data:
 447                    add_quantity(ar_cls.Quantity.RMSE, pt, data['RMSE'], index)
 448                    if 'RMSE_units' in data:
 449                        self._set_units(data['RMSE_units'],
 450                                        ar_cls.Quantity.RMSE, pt, index)
 451                if 'Cov_matrix' in data:
 452                    add_quantity(ar_cls.Quantity.Cov_matrix,
 453                                 pt, data['Cov_matrix'], index)
 454                    if 'Cov_matrix_units' in data:
 455                        self._set_units(
 456                            data['Cov_matrix_units'], ar_cls.Quantity.Cov_matrix, pt, index)
 457
 458            if data_AntiStokes is not None:
 459                data_AntiStokes = var_to_singleton(data_AntiStokes)
 460                for i, d_as in enumerate(data_AntiStokes):
 461                    add_data_pt(ar_cls.PeakType.AntiStokes, d_as, i)
 462            if data_Stokes is not None:
 463                data_Stokes = var_to_singleton(data_Stokes)
 464                for i, d_s in enumerate(data_Stokes):
 465                    add_data_pt(ar_cls.PeakType.Stokes, d_s, i)
 466            if fit_model is not None:
 467                sync(self._file.create_attr(ar_group, 'Fit_model', fit_model.value))
 468
 469        def get_units(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0) -> str:
 470            """
 471            Retrieve the units of a specified quantity from the data file.
 472
 473            Args:
 474                qt (Quantity): The quantity for which the units are to be retrieved.
 475                pt (PeakType, optional): The type of peak (e.g., Stokes or AntiStokes). Defaults to PeakType.AntiStokes.
 476                index (int, optional): The index of the quantity in case multiple quantities exist. Defaults to 0.
 477
 478            Returns:
 479                str: The units of the specified quantity as a string.
 480            """
 481            dt_name = Data.AnalysisResults._get_quantity_name(qt, pt, index)
 482            full_path = concatenate_paths(self._path, dt_name)
 483            return sync(units.of_object(self._file, full_path))
 484
 485        def _set_units(self, un: str, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0) -> str:
 486            """
 487            Set the units of a specified quantity.
 488
 489            Args:
 490                un (str): The units to be set.
 491                qt (Quantity): The quantity for which the units are to be set.
 492                pt (PeakType, optional): The type of peak (e.g., Stokes or AntiStokes). Defaults to PeakType.AntiStokes.
 493                index (int, optional): The index of the quantity in case multiple quantities exist. Defaults to 0.
 494
 495            Returns:
 496                str: The units of the specified quantity as a string.
 497            """
 498            dt_name = Data.AnalysisResults._get_quantity_name(qt, pt, index)
 499            full_path = concatenate_paths(self._path, dt_name)
 500            return units.add_to_object(self._file, full_path, un)
 501        
 502        @property
 503        def fit_model(self) -> 'Data.AnalysisResults.FitModel':
 504            """
 505            Retrieve the fit model used for the analysis.
 506
 507            Returns:
 508                Data.AnalysisResults.FitModel: The fit model used for the analysis.
 509            """
 510            if not hasattr(self, '_fit_model'):
 511                try:
 512                    fit_model_str = sync(self._file.get_attr(self._path, 'Fit_model'))
 513                    self._fit_model = Data.AnalysisResults.FitModel(fit_model_str)
 514                except Exception as e:
 515                    if isinstance(e, ValueError):
 516                        warnings.warn(
 517                            f"Unknown fit model '{fit_model_str}' found in the file.")
 518                    self._fit_model = Data.AnalysisResults.FitModel.Undefined        
 519            return self._fit_model
 520
 521        def save_image_to_OMETiff(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0, filename: str = None) -> str:
 522            """
 523            Saves the image corresponding to the specified quantity and index to an OMETiff file.
 524
 525            Args:
 526                qt (Quantity): The quantity to retrieve the image for (e.g. shift).
 527                pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes).
 528                index (int, optional): The index of the data to retrieve, if multiple are present (default is 0).
 529                filename (str, optional): The name of the file to save the image to. If None, a default name will be used.
 530
 531            Returns:
 532                str: The path to the saved OMETiff file.
 533            """
 534            try:
 535                import tifffile
 536            except ImportError:
 537                raise ModuleNotFoundError(
 538                    "The tifffile module is required for saving to OME-Tiff. Please install it using 'pip install tifffile'.")
 539            
 540            if filename is None:
 541                filename = f"{qt.value}_{pt.value}_{index}.ome.tif"
 542            if not filename.endswith('.ome.tif'):
 543                filename += '.ome.tif'
 544            img, px_size = self.get_image(qt, pt, index)
 545            if img.ndim > 3:
 546                raise NotImplementedError(
 547                    "Saving images with more than 3 dimensions is not supported yet.")
 548            with tifffile.TiffWriter(filename, bigtiff=True) as tif:
 549                metadata = {
 550                    'axes': 'ZYX',
 551                    'PhysicalSizeX': px_size[2].value,
 552                    'PhysicalSizeXUnit': px_size[2].units,
 553                    'PhysicalSizeY': px_size[1].value,
 554                    'PhysicalSizeYUnit': px_size[1].units,
 555                    'PhysicalSizeZ': px_size[0].value,
 556                    'PhysicalSizeZUnit': px_size[0].units,
 557                }
 558                tif.write(img, metadata=metadata)
 559            return filename
 560
 561        def get_image(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0) -> tuple:
 562            """
 563            Retrieves an image (spatial map) based on the specified quantity, peak type, and index.
 564
 565            Args:
 566                qt (Quantity): The quantity to retrieve the image for (e.g. shift).
 567                pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes).
 568                index (int, optional): The index of the data to retrieve, if multiple are present (default is 0).
 569
 570            Returns:
 571                A tuple containing the image corresponding to the specified quantity and index and the corresponding pixel size.
 572                The image is a 3D dataset where the dimensions are z, y, x.
 573                If there are additional parameters, more dimensions are added in the order z, y, x, par1, par2, ...
 574                The pixel size is a tuple of 3 Metadata.Item in the order z, y, x.
 575            """
 576            pt_type = Data.AnalysisResults.PeakType
 577            data = None
 578            if pt == pt_type.average:
 579                peaks = self.list_existing_peak_types(index)
 580                match len(peaks):
 581                    case 0:
 582                        raise ValueError(
 583                            "No peaks found for the specified index. Cannot compute average.")
 584                    case 1:
 585                        data = np.array(sync(self._get_quantity(qt, peaks[0], index)))
 586                    case 2:
 587                        data1, data2 = _gather_sync(
 588                            self._get_quantity(qt, peaks[0], index),
 589                            self._get_quantity(qt, peaks[1], index)
 590                            )
 591                        data = (np.abs(data1) + np.abs(data2))/2
 592            else:
 593                data = np.array(sync(self._get_quantity(qt, pt, index)))
 594            sm = np.array(self._spatial_map)
 595            img = data[sm, ...]
 596            img[sm<0, ...] = np.nan  # set invalid pixels to NaN
 597            return img, self._spatial_map_px_size
 598        def get_quantity_at_pixel(self, coord: tuple, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0):
 599            """
 600            Synchronous wrapper for `get_quantity_at_pixel_async` (see doc for `brimfile.data.Data.AnalysisResults.get_quantity_at_pixel_async`)
 601            """
 602            return sync(self.get_quantity_at_pixel_async(coord, qt, pt, index))
 603        async def get_quantity_at_pixel_async(self, coord: tuple, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0):
 604            """
 605            Retrieves the specified quantity in the image at coord, based on the peak type and index.
 606
 607            Args:
 608                coord (tuple): A tuple of 3 elements corresponding to the z, y, x coordinate in the image
 609                qt (Quantity): The quantity to retrieve the image for (e.g. shift).
 610                pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes).
 611                index (int, optional): The index of the data to retrieve, if multiple peaks are present (default is 0).
 612
 613            Returns:
 614                The requested quantity, which is a scalar or a multidimensional array (depending on whether there are additional parameters in the current Data group)
 615            """
 616            if len(coord) != 3:
 617                raise ValueError(
 618                    "'coord' must have 3 elements corresponding to z, y, x")
 619            i = self._spatial_map[*coord]
 620            assert i.size == 1
 621            if i<0:
 622                return np.nan  # invalid pixel
 623            i = int(i)
 624
 625            pt_type = Data.AnalysisResults.PeakType
 626            value = None
 627            if pt == pt_type.average:
 628                value = None
 629                peaks = await self.list_existing_peak_types_async(index)
 630                match len(peaks):
 631                    case 0:
 632                        raise ValueError(
 633                            "No peaks found for the specified index. Cannot compute average.")
 634                    case 1:
 635                        data = await self._get_quantity(qt, peaks[0], index)
 636                        value = await _async_getitem(data, (i, ...))
 637                    case 2:
 638                        data_p0, data_p1 = await asyncio.gather(
 639                            self._get_quantity(qt, peaks[0], index),
 640                            self._get_quantity(qt, peaks[1], index)
 641                        )
 642                        value1, value2 = await asyncio.gather(
 643                            _async_getitem(data_p0, (i, ...)),
 644                            _async_getitem(data_p1, (i, ...))
 645                        )
 646                        value = (np.abs(value1) + np.abs(value2))/2
 647            else:
 648                data = await self._get_quantity(qt, pt, index)
 649                value = await _async_getitem(data, (i, ...))
 650            return value
 651        def get_all_quantities_in_image(self, coor: tuple, index_peak: int = 0) -> dict:
 652            """
 653            Retrieve all available quantities at a specific spatial coordinate.
 654            see `brimfile.data.Data.AnalysisResults._get_all_quantities_at_index` for more details
 655            TODO complete the documentation
 656            """
 657            if len(coor) != 3:
 658                raise ValueError("coor must contain 3 values for z, y, x")
 659            index = int(self._spatial_map[coor])
 660            return sync(self._get_all_quantities_at_index(index, index_peak))
 661        async def _get_all_quantities_at_index(self, index: int, index_peak: int = 0) -> dict:
 662            """
 663            Retrieve all available quantities for a specific spatial index.
 664            Args:
 665                index (int): The spatial index to retrieve quantities for.
 666                index_peak (int, optional): The index of the data to retrieve, if multiple peaks are present (default is 0).
 667            Returns:
 668                dict: A dictionary of Metadata.Item in the form `result[quantity.name][peak.name] = bls.Metadata.Item(value, units)`
 669            """
 670            async def _get_existing_quantity_at_index_async(self,  pt: Data.AnalysisResults.PeakType = Data.AnalysisResults.PeakType.AntiStokes):
 671                as_cls = Data.AnalysisResults
 672                qts_ls = ()
 673                dts_ls = ()
 674
 675                qts = [qt for qt in as_cls.Quantity]
 676                coros = [self._file.open_dataset(concatenate_paths(self._path, as_cls._get_quantity_name(qt, pt, index_peak))) for qt in qts]
 677                
 678                # open the datasets asynchronously, excluding those that do not exist
 679                opened_dts = await asyncio.gather(*coros, return_exceptions=True)
 680                for i, opened_qt in enumerate(opened_dts):
 681                    if not isinstance(opened_qt, Exception):
 682                        qts_ls += (qts[i],)
 683                        dts_ls += (opened_dts[i],)
 684                # get the values at the specified index
 685                coros_values = [_async_getitem(dt, (index, ...)) for dt in dts_ls]
 686                coros_units = [units.of_object(self._file, dt) for dt in dts_ls]
 687                ret_ls = await asyncio.gather(*coros_values, *coros_units)
 688                n = len(coros_values)
 689                value_ls = [Metadata.Item(ret_ls[i], ret_ls[n+i]) for i in range(n)]
 690                return qts_ls, value_ls
 691            antiStokes, stokes = await asyncio.gather(
 692                _get_existing_quantity_at_index_async(self, Data.AnalysisResults.PeakType.AntiStokes),
 693                _get_existing_quantity_at_index_async(self, Data.AnalysisResults.PeakType.Stokes)
 694            )
 695            res = {}
 696            # combine the results, including the average
 697            for qt in (set(antiStokes[0]) | set(stokes[0])):
 698                res[qt.name] = {}
 699                pts = ()
 700                #Stokes
 701                if qt in stokes[0]:
 702                    res[qt.name][Data.AnalysisResults.PeakType.Stokes.name] = stokes[1][stokes[0].index(qt)]
 703                    pts += (Data.AnalysisResults.PeakType.Stokes,)
 704                #AntiStokes
 705                if qt in antiStokes[0]:
 706                    res[qt.name][Data.AnalysisResults.PeakType.AntiStokes.name] = antiStokes[1][antiStokes[0].index(qt)]
 707                    pts += (Data.AnalysisResults.PeakType.AntiStokes,)
 708                #average getting the units of the first peak
 709                res[qt.name][Data.AnalysisResults.PeakType.average.name] = Metadata.Item(
 710                    np.mean([np.abs(res[qt.name][pt.name].value) for pt in pts]), 
 711                    res[qt.name][pts[0].name].units
 712                    )
 713                if not all(res[qt.name][pt.name].units == res[qt.name][pts[0].name].units for pt in pts):
 714                    warnings.warn(f"The units of {pts} are not consistent.")
 715            return res
 716
 717        @classmethod
 718        def _get_quantity_name(cls, qt: Quantity, pt: PeakType, index: int) -> str:
 719            """
 720            Returns the name of the dataset correponding to the specific Quantity, PeakType and index
 721
 722            Args:
 723                qt (Quantity)   
 724                pt (PeakType)  
 725                intex (int): in case of multiple peaks fitted, the index of the peak to consider       
 726            """
 727            if not pt in (cls.PeakType.AntiStokes, cls.PeakType.Stokes):
 728                raise ValueError("pt has to be either Stokes or AntiStokes")
 729            if qt == cls.Quantity.R2 or qt == cls.Quantity.RMSE or qt == cls.Quantity.Cov_matrix:
 730                name = f"Fit_error_{str(pt.value)}_{index}/{str(qt.value)}"
 731            else:
 732                name = f"{str(qt.value)}_{str(pt.value)}_{index}"
 733            return name
 734
 735        async def _get_quantity(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0):
 736            """
 737            Retrieve a specific quantity dataset from the file.
 738
 739            Args:
 740                qt (Quantity): The type of quantity to retrieve.
 741                pt (PeakType, optional): The peak type to consider (default is PeakType.AntiStokes).
 742                index (int, optional): The index of the quantity if multiple peaks are available (default is 0).
 743
 744            Returns:
 745                The dataset corresponding to the specified quantity, as stored in the file.
 746
 747            """
 748
 749            dt_name = Data.AnalysisResults._get_quantity_name(qt, pt, index)
 750            full_path = concatenate_paths(self._path, dt_name)
 751            return await self._file.open_dataset(full_path)
 752
 753        def list_existing_peak_types(self, index: int = 0) -> tuple:
 754            """
 755            Synchronous wrapper for `list_existing_peak_types_async` (see doc for `brimfile.data.Data.AnalysisResults.list_existing_peak_types_async`)
 756            """
 757            return sync(self.list_existing_peak_types_async(index)) 
 758        async def list_existing_peak_types_async(self, index: int = 0) -> tuple:
 759            """
 760            Returns a tuple of existing peak types (Stokes and/or AntiStokes) for the specified index.
 761            Args:
 762                index (int, optional): The index of the peak to check (in case of multi-peak fit). Defaults to 0.
 763            Returns:
 764                tuple: A tuple containing `PeakType` members (`Stokes`, `AntiStokes`) that exist for the given index.
 765            """
 766
 767            as_cls = Data.AnalysisResults
 768            shift_s_name = as_cls._get_quantity_name(
 769                as_cls.Quantity.Shift, as_cls.PeakType.Stokes, index)
 770            shift_as_name = as_cls._get_quantity_name(
 771                as_cls.Quantity.Shift, as_cls.PeakType.AntiStokes, index)
 772            ls = ()
 773            coro_as_exists = self._file.object_exists(concatenate_paths(self._path, shift_as_name))
 774            coro_s_exists = self._file.object_exists(concatenate_paths(self._path, shift_s_name))
 775            as_exists, s_exists = await asyncio.gather(coro_as_exists, coro_s_exists)
 776            if as_exists:
 777                ls += (as_cls.PeakType.AntiStokes,)
 778            if s_exists:
 779                ls += (as_cls.PeakType.Stokes,)
 780            return ls
 781
 782        def list_existing_quantities(self,  pt: PeakType = PeakType.AntiStokes, index: int = 0) -> tuple:
 783            """
 784            Synchronous wrapper for `list_existing_quantities_async` (see doc for `brimfile.data.Data.AnalysisResults.list_existing_quantities_async`)
 785            """
 786            return sync(self.list_existing_quantities_async(pt, index))
 787        async def list_existing_quantities_async(self,  pt: PeakType = PeakType.AntiStokes, index: int = 0) -> tuple:
 788            """
 789            Returns a tuple of existing quantities for the specified index.
 790            Args:
 791                index (int, optional): The index of the peak to check (in case of multi-peak fit). Defaults to 0.
 792            Returns:
 793                tuple: A tuple containing `Quantity` members that exist for the given index.
 794            """
 795            as_cls = Data.AnalysisResults
 796            ls = ()
 797
 798            qts = [qt for qt in as_cls.Quantity]
 799            coros = [self._file.object_exists(concatenate_paths(self._path, as_cls._get_quantity_name(qt, pt, index))) for qt in qts]
 800            
 801            qt_exists = await asyncio.gather(*coros)
 802            for i, exists in enumerate(qt_exists):
 803                if exists:
 804                    ls += (qts[i],)
 805            return ls
 806
 807    def get_metadata(self):
 808        """
 809        Returns the metadata associated with the current Data group
 810        Note that this contains both the general metadata stored in the file (which might be redifined by the specific data group)
 811        and the ones specific for this data group
 812        """
 813        return Metadata(self._file, self._path)
 814
 815    def get_num_parameters(self) -> tuple:
 816        """
 817        Retrieves the number of parameters
 818
 819        Returns:
 820            tuple: The shape of the parameters if they exist, otherwise an empty tuple.
 821        """
 822        pars, _ = self.get_parameters()
 823        return pars.shape if pars is not None else ()
 824
 825    def get_parameters(self) -> list:
 826        """
 827        Retrieves the parameters  and their associated names.
 828
 829        If PSD.ndims > 2, the parameters are stored in a separate dataset.
 830
 831        Returns:
 832            list: A tuple containing the parameters and their names if there are any, otherwise None.
 833        """
 834        pars_full_path = concatenate_paths(
 835            self._path, brim_obj_names.data.parameters)
 836        if sync(self._file.object_exists(pars_full_path)):
 837            pars = sync(self._file.open_dataset(pars_full_path))
 838            pars_names = sync(self._file.get_attr(pars, 'Name'))
 839            return (pars, pars_names)
 840        return (None, None)
 841
 842    def create_analysis_results_group(self, data_AntiStokes, data_Stokes=None, index: int = None, name: str = None, fit_model: 'Data.AnalysisResults.FitModel' = None) -> AnalysisResults:
 843        """
 844        Adds a new AnalysisResults entry to the current data group.
 845        Parameters:
 846            data_AntiStokes (dict or list[dict]): contains the same elements as the ones in `AnalysisResults.add_data`,
 847                but all the quantities (i.d. 'shift', 'width', etc.) are 3D, corresponding to the spatial positions (z, y, x).
 848            data_Stokes (dict or list[dict]): same as data_AntiStokes for the Stokes peaks.
 849            index (int, optional): The index for the new data entry. If None, the next available index is used. Defaults to None.
 850            name (str, optional): The name for the new Analysis group. Defaults to None.
 851            fit_model (Data.AnalysisResults.FitModel, optional): The fit model used for the analysis. Defaults to None (no attribute is set).
 852        Returns:
 853            AnalysisResults: The newly created AnalysisResults object.
 854        Raises:
 855            IndexError: If the specified index already exists in the dataset.
 856            ValueError: If any of the data provided is not valid or consistent
 857        """
 858        def flatten_data(data: dict):
 859            if data is None:
 860                return None
 861            data = var_to_singleton(data)
 862            out_data = []
 863            for dn in data:
 864                for k in dn.keys():
 865                    if not k.endswith('_units'):
 866                        d = dn[k]
 867                        if d.ndim != 3 or d.shape != self._spatial_map.shape:
 868                            raise ValueError(
 869                                f"'{k}' must have 3 dimensions (z, y, x) and same shape as the spatial map ({self._spatial_map.shape})")
 870                        dn[k] = np.reshape(d, -1)  # flatten the data
 871                out_data.append(dn)
 872            return out_data
 873        data_AntiStokes = flatten_data(data_AntiStokes)
 874        data_Stokes = flatten_data(data_Stokes)
 875        return self.create_analysis_results_group_raw(data_AntiStokes, data_Stokes, index, name, fit_model=fit_model)
 876
 877    def create_analysis_results_group_raw(self, data_AntiStokes, data_Stokes=None, index: int = None, name: str = None, fit_model: 'Data.AnalysisResults.FitModel' = None) -> AnalysisResults:
 878        """
 879        Adds a new AnalysisResults entry to the current data group.
 880        Parameters:
 881            data_AntiStokes (dict or list[dict]): see documentation for AnalysisResults.add_data
 882            data_Stokes (dict or list[dict]): same as data_AntiStokes for the Stokes peaks.
 883            index (int, optional): The index for the new data entry. If None, the next available index is used. Defaults to None.
 884            name (str, optional): The name for the new Analysis group. Defaults to None.
 885            fit_model (Data.AnalysisResults.FitModel, optional): The fit model used for the analysis. Defaults to None (no attribute is set).
 886        Returns:
 887            AnalysisResults: The newly created AnalysisResults object.
 888        Raises:
 889            IndexError: If the specified index already exists in the dataset.
 890            ValueError: If any of the data provided is not valid or consistent
 891        """
 892        if index is not None:
 893            try:
 894                self.get_analysis_results(index)
 895            except IndexError:
 896                pass
 897            else:
 898                # If the group already exists, raise an error
 899                raise IndexError(
 900                    f"Analysis {index} already exists in {self._path}")
 901        else:
 902            ar_groups = self.list_AnalysisResults()
 903            indices = [ar['index'] for ar in ar_groups]
 904            indices.sort()
 905            index = indices[-1] + 1 if indices else 0  # Next available index
 906
 907        ar = Data.AnalysisResults._create_new(self, index)
 908        if name is not None:
 909            set_object_name(self._file, ar._path, name)
 910        ar.add_data(data_AntiStokes, data_Stokes, fit_model=fit_model)
 911
 912        return ar
 913
 914    def list_AnalysisResults(self, retrieve_custom_name=False) -> list:
 915        """
 916        List all AnalysisResults groups in the current data group. The list is ordered by index.
 917
 918        Returns:
 919            list: A list of dictionaries, each containing:
 920                - 'name' (str): The name of the AnalysisResults group.
 921                - 'index' (int): The index extracted from the group name.
 922                - 'custom_name' (str, optional): if retrieve_custom_name==True, it contains the name of the AnalysisResults group as returned from utils.get_object_name.
 923        """
 924
 925        analysis_results_groups = []
 926
 927        matched_objs = list_objects_matching_pattern(
 928            self._file, self._group, brim_obj_names.data.analysis_results + r"_(\d+)$")
 929        async def _make_dict_item(matched_obj, retrieve_custom_name):
 930            name = matched_obj[0]
 931            index = int(matched_obj[1])
 932            curr_obj_dict = {'name': name, 'index': index}
 933            if retrieve_custom_name:
 934                ar_path = concatenate_paths(self._path, name)
 935                custom_name = await get_object_name(self._file, ar_path)
 936                curr_obj_dict['custom_name'] = custom_name
 937            return curr_obj_dict
 938        coros = [_make_dict_item(matched_obj, retrieve_custom_name) for matched_obj in matched_objs]
 939        dicts = _gather_sync(*coros)
 940        for dict_item in dicts:
 941            analysis_results_groups.append(dict_item)
 942        # Sort the data groups by index
 943        analysis_results_groups.sort(key=lambda x: x['index'])
 944
 945        return analysis_results_groups
 946
 947    def get_analysis_results(self, index: int = 0) -> AnalysisResults:
 948        """
 949        Returns the AnalysisResults at the specified index
 950
 951        Args:
 952            index (int)                
 953
 954        Raises:
 955            IndexError: If there is no analysis with the corresponding index
 956        """
 957        name = None
 958        ls = self.list_AnalysisResults()
 959        for el in ls:
 960            if el['index'] == index:
 961                name = el['name']
 962                break
 963        if name is None:
 964            raise IndexError(f"Analysis {index} not found")
 965        path = concatenate_paths(self._path, name)
 966        return Data.AnalysisResults(self._file, path, self._spatial_map, self._spatial_map_px_size)
 967
 968    def add_data(self, PSD: np.ndarray, frequency: np.ndarray, scanning: dict, freq_units='GHz', timestamp: np.ndarray = None, compression: FileAbstraction.Compression = FileAbstraction.Compression()):
 969        """
 970        Add data to the current data group.
 971
 972        This method adds the provided PSD, frequency, and scanning data to the HDF5 group 
 973        associated with this `Data` object. It validates the inputs to ensure they meet 
 974        the required specifications before adding them.
 975
 976        Args:
 977            PSD (np.ndarray): A 2D numpy array representing the Power Spectral Density (PSD) data. The last dimension contains the spectra.
 978            frequency (np.ndarray): A 1D or 2D numpy array representing the frequency data. 
 979                It must be broadcastable to the shape of the PSD array.
 980            scanning (dict): A dictionary containing scanning-related data. It may include:
 981                - 'Spatial_map' (optional): A dictionary containing (up to) 3 arrays (x, y, z) and a string (units)
 982                - 'Cartesian_visualisation' (optional): A 3D numpy array containing the association between spatial position and spectra.
 983                   It must have integer values between 0 and PSD.shape[0]-1, or -1 for invalid entries.
 984                - 'Cartesian_visualisation_pixel' (optional): A list or array of 3 float values 
 985                  representing the pixel size in the z, y, and x dimensions (unused dimensions can be set to None).
 986                - 'Cartesian_visualisation_pixel_unit' (optional): A string representing the unit of the pixel size (e.g. 'um').
 987            timestamp (np.ndarray): the timestamp associated with each spectrum.
 988                It must be a 1D array with the same length as the PSD array.
 989
 990
 991        Raises:
 992            ValueError: If any of the data provided is not valid or consistent
 993        """
 994
 995        # Check if frequency is broadcastable to PSD
 996        try:
 997            np.broadcast_shapes(tuple(frequency.shape), tuple(PSD.shape))
 998        except ValueError as e:
 999            raise ValueError(f"frequency (shape: {frequency.shape}) is not broadcastable to PSD (shape: {PSD.shape}): {e}")
1000
1001        # define the scanning_is_valid variable to check if at least one of 'Spatial_map' or 'Cartesian_visualisation'
1002        # is present in the scanning dictionary
1003        scanning_is_valid = False
1004        if 'Spatial_map' in scanning:
1005            sm = scanning['Spatial_map']
1006            size = 0
1007
1008            def check_coor(coor: str):
1009                if coor in sm:
1010                    sm[coor] = np.array(sm[coor])
1011                    size1 = sm[coor].size
1012                    if size1 != size and size != 0:
1013                        raise ValueError(
1014                            f"'{coor}' in 'Spatial_map' is invalid!")
1015                    return size1
1016            size = check_coor('x')
1017            size = check_coor('y')
1018            size = check_coor('z')
1019            if size == 0:
1020                raise ValueError(
1021                    "'Spatial_map' should contain at least one x, y or z")
1022            scanning_is_valid = True
1023        if 'Cartesian_visualisation' in scanning:
1024            cv = scanning['Cartesian_visualisation']
1025            if not isinstance(cv, np.ndarray) or cv.ndim != 3:
1026                raise ValueError(
1027                    "Cartesian_visualisation must be a 3D numpy array")
1028            if not np.issubdtype(cv.dtype, np.integer) or np.min(cv) < -1 or np.max(cv) >= PSD.shape[0]:
1029                raise ValueError(
1030                    "Cartesian_visualisation values must be integers between -1 and PSD.shape[0]-1")
1031            if 'Cartesian_visualisation_pixel' in scanning:
1032                if len(scanning['Cartesian_visualisation_pixel']) != 3:
1033                    raise ValueError(
1034                        "Cartesian_visualisation_pixel must always contain 3 values for z, y, x (set to None if not used)")
1035            else:
1036                warnings.warn(
1037                    "It is recommended to add 'Cartesian_visualisation_pixel' to the scanning dictionary, to define the pixel size")
1038            scanning_is_valid = True
1039        if not scanning_is_valid:
1040            raise ValueError("scanning is not valid")
1041
1042        if timestamp is not None:
1043            if not isinstance(timestamp, np.ndarray) or timestamp.ndim != 1 or len(timestamp) != PSD.shape[0]:
1044                raise ValueError("timestamp is not compatible with PSD")
1045
1046        # TODO: add and validate additional datasets (i.e. 'Parameters', 'Calibration_index', etc.)
1047
1048        # Add datasets to the group
1049        def determine_chunk_size(arr: np.array) -> tuple:
1050            """"
1051            Use the same heuristic as the zarr library to determine the chunk size, but without splitting the last dimension
1052            """
1053            shape = arr.shape
1054            typesize = arr.itemsize
1055            #if the array is 1D, do not chunk it
1056            if len(shape) <= 1:
1057                return (shape[0],)
1058            target_sizes = _guess_chunks.__kwdefaults__
1059            # divide the target size by the last dimension size to get the chunk size for the other dimensions
1060            target_sizes = {k: target_sizes[k] // shape[-1] 
1061                            for k in target_sizes.keys()}
1062            chunks = _guess_chunks(shape[0:-1], typesize, arr.nbytes, **target_sizes)
1063            return chunks + (shape[-1],)  # keep the last dimension size unchanged
1064        sync(self._file.create_dataset(
1065            self._group, brim_obj_names.data.PSD, data=PSD,
1066            chunk_size=determine_chunk_size(PSD), compression=compression))
1067        freq_ds = sync(self._file.create_dataset(
1068            self._group,  brim_obj_names.data.frequency, data=frequency,
1069            chunk_size=determine_chunk_size(frequency), compression=compression))
1070        units.add_to_object(self._file, freq_ds, freq_units)
1071
1072        if 'Spatial_map' in scanning:
1073            sm = scanning['Spatial_map']
1074            sm_group = sync(self._file.create_group(concatenate_paths(
1075                self._path, brim_obj_names.data.spatial_map)))
1076            if 'units' in sm:
1077                units.add_to_object(self._file, sm_group, sm['units'])
1078
1079            def add_sm_dataset(coord: str):
1080                if coord in sm:
1081                    coord_dts = sync(self._file.create_dataset(
1082                        sm_group, coord, data=sm[coord], compression=compression))
1083
1084            add_sm_dataset('x')
1085            add_sm_dataset('y')
1086            add_sm_dataset('z')
1087        if 'Cartesian_visualisation' in scanning:
1088            # convert the Cartesian_visualisation to the smallest integer type
1089            cv_arr = np_array_to_smallest_int_type(scanning['Cartesian_visualisation'])
1090            cv = sync(self._file.create_dataset(self._group, brim_obj_names.data.cartesian_visualisation,
1091                                           data=cv_arr, compression=compression))
1092            if 'Cartesian_visualisation_pixel' in scanning:
1093                sync(self._file.create_attr(
1094                    cv, 'element_size', scanning['Cartesian_visualisation_pixel']))
1095                if 'Cartesian_visualisation_pixel_unit' in scanning:
1096                    px_unit = scanning['Cartesian_visualisation_pixel_unit']
1097                else:
1098                    warnings.warn(
1099                        "No unit provided for Cartesian_visualisation_pixel, defaulting to 'um'")
1100                    px_unit = 'um'
1101                units.add_to_attribute(self._file, cv, 'element_size', px_unit)
1102
1103        self._spatial_map, self._spatial_map_px_size = self._load_spatial_mapping()
1104
1105        if timestamp is not None:
1106            sync(self._file.create_dataset(
1107                self._group, 'Timestamp', data=timestamp, compression=compression))
1108
1109    @staticmethod
1110    def list_data_groups(file: FileAbstraction, retrieve_custom_name=False) -> list:
1111        """
1112        List all data groups in the brim file. The list is ordered by index.
1113
1114        Returns:
1115            list: A list of dictionaries, each containing:
1116                - 'name' (str): The name of the data group in the file.
1117                - 'index' (int): The index extracted from the group name.
1118                - 'custom_name' (str, optional): if retrieve_custom_name==True, it contains the name of the data group as returned from utils.get_object_name.
1119        """
1120
1121        data_groups = []
1122
1123        matched_objs = list_objects_matching_pattern(
1124            file, brim_obj_names.Brillouin_base_path, brim_obj_names.data.base_group + r"_(\d+)$")
1125        
1126        async def _make_dict_item(matched_obj, retrieve_custom_name):
1127            name = matched_obj[0]
1128            index = int(matched_obj[1])
1129            curr_obj_dict = {'name': name, 'index': index}
1130            if retrieve_custom_name:
1131                path = concatenate_paths(
1132                    brim_obj_names.Brillouin_base_path, name)
1133                custom_name = await get_object_name(file, path)
1134                curr_obj_dict['custom_name'] = custom_name
1135            return curr_obj_dict
1136        
1137        coros = [_make_dict_item(matched_obj, retrieve_custom_name) for matched_obj in matched_objs]
1138        dicts = _gather_sync(*coros)
1139        for dict_item in dicts:
1140            data_groups.append(dict_item)        
1141        # Sort the data groups by index
1142        data_groups.sort(key=lambda x: x['index'])
1143
1144        return data_groups
1145
1146    @staticmethod
1147    def _get_existing_group_name(file: FileAbstraction, index: int) -> str:
1148        """
1149        Get the name of an existing data group by index.
1150
1151        Args:
1152            file (File): The parent File object.
1153            index (int): The index of the data group.
1154
1155        Returns:
1156            str: The name of the data group, or None if not found.
1157        """
1158        group_name: str = None
1159        data_groups = Data.list_data_groups(file)
1160        for dg in data_groups:
1161            if dg['index'] == index:
1162                group_name = dg['name']
1163                break
1164        return group_name
1165
1166    @classmethod
1167    def _create_new(cls, file: FileAbstraction, index: int, name: str = None) -> 'Data':
1168        """
1169        Create a new data group with the specified index.
1170
1171        Args:
1172            file (File): The parent File object.
1173            index (int): The index for the new data group.
1174            name (str, optional): The name for the new data group. Defaults to None.
1175
1176        Returns:
1177            Data: The newly created Data object.
1178        """
1179        group_name = Data._generate_group_name(index)
1180        group = sync(file.create_group(concatenate_paths(
1181            brim_obj_names.Brillouin_base_path, group_name)))
1182        if name is not None:
1183            set_object_name(file, group, name)
1184        return cls(file, concatenate_paths(brim_obj_names.Brillouin_base_path, group_name))
1185
1186    @staticmethod
1187    def _generate_group_name(index: int, n_digits: int = None) -> str:
1188        """
1189        Generate a name for a data group based on the index.
1190
1191        Args:
1192            index (int): The index for the data group.
1193            n_digits (int, optional): The number of digits to pad the index with. If None no padding is applied. Defaults to None.
1194
1195        Returns:
1196            str: The generated group name.
1197
1198        Raises:
1199            ValueError: If the index is negative.
1200        """
1201        if index < 0:
1202            raise ValueError("index must be positive")
1203        num = str(index)
1204        if n_digits is not None:
1205            num = num.zfill(n_digits)
1206        return f"{brim_obj_names.data.base_group}_{num}"
class Data:
  22class Data:
  23    """
  24    Represents a data group within the brim file.
  25    """
  26
  27    def __init__(self, file: FileAbstraction, path: str):
  28        """
  29        Initialize the Data object. This constructor should not be called directly.
  30
  31        Args:
  32            file (File): The parent File object.
  33            path (str): The path to the data group within the file.
  34        """
  35        self._file = file
  36        self._path = path
  37        self._group = sync(file.open_group(path))
  38
  39        self._spatial_map, self._spatial_map_px_size = self._load_spatial_mapping()
  40
  41    def get_name(self):
  42        """
  43        Returns the name of the data group.
  44        """
  45        return sync(get_object_name(self._file, self._path))
  46    
  47    def get_index(self):
  48        """
  49        Returns the index of the data group.
  50        """
  51        return int(self._path.split('/')[-1].split('_')[-1])
  52
  53    def _load_spatial_mapping(self, load_in_memory: bool=True) -> tuple:
  54        """
  55        Load a spatial mapping in the same format as 'Cartesian visualisation',
  56        irrespectively on whether 'Spatial_map' is defined instead.
  57        -1 is used for "empty" pixels in the image
  58        Args:
  59            load_in_memory (bool): Specify whether the map should be forced to load in memory or just opened as a dataset.
  60        Returns:
  61            The spatial map and the corresponding pixel size as a tuple of 3 Metadata.Item, both in the order z, y, x.
  62        """
  63        cv = None
  64        px_size = 3*(Metadata.Item(value=1, units=None),)
  65
  66        cv_path = concatenate_paths(
  67            self._path, brim_obj_names.data.cartesian_visualisation)
  68        sm_path = concatenate_paths(
  69            self._path, brim_obj_names.data.spatial_map)
  70        
  71        if sync(self._file.object_exists(cv_path)):
  72            cv = sync(self._file.open_dataset(cv_path))
  73
  74            #read the pixel size from the 'Cartesian visualisation' dataset
  75            px_size_val = None
  76            px_size_units = None
  77            try:
  78                px_size_val = sync(self._file.get_attr(cv, 'element_size'))
  79                if px_size_val is None or len(px_size_val) != 3:
  80                    raise ValueError(
  81                        "The 'element_size' attribute of 'Cartesian_visualisation' must be a tuple of 3 elements")
  82            except Exception:
  83                px_size_val = 3*(1,)
  84                warnings.warn(
  85                    "No pixel size defined for Cartesian visualisation")            
  86            px_size_units = sync(units.of_attribute(
  87                    self._file, cv, 'element_size'))
  88            px_size = ()
  89            for i in range(3):
  90                # if px_size_val[i] is not a number, set it to 1 and px_size_units to None
  91                if isinstance(px_size_val[i], Number):
  92                    px_size += (Metadata.Item(px_size_val[i], px_size_units), )
  93                else:
  94                    px_size += (Metadata.Item(1, None), )
  95                    
  96
  97            if load_in_memory:
  98                cv = np.array(cv)
  99                cv = np_array_to_smallest_int_type(cv)
 100
 101        elif sync(self._file.object_exists(sm_path)):
 102            def load_spatial_map_from_file(self):
 103                async def load_coordinate_from_sm(coord: str):
 104                    res = np.empty(0)  # empty array
 105                    try:
 106                        res = await self._file.open_dataset(
 107                            concatenate_paths(sm_path, coord))
 108                        res = await res.to_np_array()
 109                        res = np.squeeze(res)  # remove single-dimensional entries
 110                    except Exception as e:
 111                        # if the coordinate does not exist, return an empty array
 112                        pass
 113                    if len(res.shape) > 1:
 114                        raise ValueError(
 115                            f"The 'Spatial_map/{coord}' dataset is not a 1D array as expected")
 116                    return res
 117
 118                def check_coord_array(arr, size):
 119                    if arr.size == 0:
 120                        return np.zeros(size)
 121                    elif arr.size != size:
 122                        raise ValueError(
 123                            "The 'Spatial_map' dataset is invalid")
 124                    return arr
 125
 126                x, y, z = _gather_sync(
 127                    load_coordinate_from_sm('x'),
 128                    load_coordinate_from_sm('y'),
 129                    load_coordinate_from_sm('z')
 130                    )
 131                size = max([x.size, y.size, z.size])
 132                if size == 0:
 133                    raise ValueError("The 'Spatial_map' dataset is empty")
 134                x = check_coord_array(x, size)
 135                y = check_coord_array(y, size)
 136                z = check_coord_array(z, size)
 137                return x, y, z
 138
 139            def calculate_step(x):
 140                n = len(np.unique(x))
 141                if n == 1:
 142                    d = None
 143                else:
 144                    d = (np.max(x)-np.min(x))/(n-1)
 145                return n, d
 146
 147            x, y, z = load_spatial_map_from_file(self)
 148
 149            # TODO extend the reconstruction to non-cartesian cases
 150
 151            nX, dX = calculate_step(x)
 152            nY, dY = calculate_step(y)
 153            nZ, dZ = calculate_step(z)
 154
 155            indices = np_array_to_smallest_int_type(np.lexsort((x, y, z)))
 156            cv = np.reshape(indices, (nZ, nY, nX))
 157
 158            px_size_units = sync(units.of_object(self._file, sm_path))
 159            px_size = ()
 160            for i in range(3):
 161                px_sz = (dZ, dY, dX)[i]
 162                px_unit = px_size_units
 163                if px_sz is None:
 164                    px_sz = 1
 165                    px_unit = None
 166                px_size += (Metadata.Item(px_sz, px_unit),)
 167
 168        return cv, px_size
 169
 170    def get_PSD(self) -> tuple:
 171        """
 172        LOW LEVEL FUNCTION
 173
 174        Retrieve the Power Spectral Density (PSD) and frequency from the current data group.
 175        Note: this function exposes the internals of the brim file and thus the interface might change in future versions.
 176        Use only if more specialized functions are not working for your application!
 177        Returns:
 178            tuple: (PSD, frequency, PSD_units, frequency_units)
 179                - PSD: A 2D (or more) numpy array containing all the spectra (see [specs](https://github.com/prevedel-lab/Brillouin-standard-file/blob/main/docs/brim_file_specs.md) for more details).
 180                - frequency: A numpy array representing the frequency data (see [specs](https://github.com/prevedel-lab/Brillouin-standard-file/blob/main/docs/brim_file_specs.md) for more details).
 181                - PSD_units: The units of the PSD.
 182                - frequency_units: The units of the frequency.
 183        """
 184        PSD, frequency = _gather_sync(
 185            self._file.open_dataset(concatenate_paths(
 186                self._path, brim_obj_names.data.PSD)),
 187            self._file.open_dataset(concatenate_paths(
 188                self._path, brim_obj_names.data.frequency))
 189        )
 190        # retrieve the units of the PSD and frequency
 191        PSD_units, frequency_units = _gather_sync(
 192            units.of_object(self._file, PSD),
 193            units.of_object(self._file, frequency)
 194        )
 195
 196        return PSD, frequency, PSD_units, frequency_units
 197    
 198    def get_PSD_as_spatial_map(self) -> tuple:
 199        """
 200        Retrieve the Power Spectral Density (PSD) as a spatial map and the frequency from the current data group.
 201        Returns:
 202            tuple: (PSD, frequency, PSD_units, frequency_units)
 203                - PSD: A 4D (or more) numpy array containing all the spectra. Dimensions are z, y, x, [parameters], spectrum.
 204                - frequency: A numpy array representing the frequency data, which can be broadcastable to PSD.
 205                - PSD_units: The units of the PSD.
 206                - frequency_units: The units of the frequency.
 207        """
 208        PSD, frequency = _gather_sync(
 209            self._file.open_dataset(concatenate_paths(
 210                self._path, brim_obj_names.data.PSD)),        
 211            self._file.open_dataset(concatenate_paths(
 212                self._path, brim_obj_names.data.frequency))
 213            )        
 214        # retrieve the units of the PSD and frequency
 215        PSD_units, frequency_units = _gather_sync(
 216            units.of_object(self._file, PSD),
 217            units.of_object(self._file, frequency)
 218        )
 219
 220        # ensure PSD and frequency are numpy arrays
 221        PSD = np.array(PSD)  
 222        frequency = np.array(frequency)  # ensure it's a numpy array
 223        
 224        #if the frequency is not the same for all spectra, broadcast it to match the shape of PSD
 225        if frequency.ndim > 1:
 226            frequency = np.broadcast_to(frequency, PSD.shape)
 227        
 228        sm = np.array(self._spatial_map)
 229        # reshape the PSD to have the spatial dimensions first      
 230        PSD = PSD[sm, ...]
 231        # reshape the frequency pnly if it is not the same for all spectra
 232        if frequency.ndim > 1:
 233            frequency = frequency[sm, ...]
 234
 235        return PSD, frequency, PSD_units, frequency_units
 236
 237    def get_spectrum(self, index: int) -> tuple:
 238        """
 239        Synchronous wrapper for `get_spectrum_async` (see doc for `brimfile.data.Data.get_spectrum_async`)
 240        """
 241        return sync(self.get_spectrum_async(index))
 242    async def get_spectrum_async(self, index: int) -> tuple:
 243        """
 244        Retrieve a spectrum from the data group.
 245
 246        Args:
 247            index (int): The index of the spectrum to retrieve.
 248
 249        Returns:
 250            tuple: (PSD, frequency, PSD_units, frequency_units) for the specified index. 
 251                    PSD can be 1D or more (if there are additional parameters);
 252                    frequency has the same size as PSD
 253        Raises:
 254            IndexError: If the index is out of range for the PSD dataset.
 255        """
 256        # index = -1 corresponds to no spectrum
 257        if index < 0:
 258            return None, None, None, None
 259        PSD, frequency = await asyncio.gather(
 260            self._file.open_dataset(concatenate_paths(
 261                self._path, brim_obj_names.data.PSD)),                       
 262            self._file.open_dataset(concatenate_paths(
 263                self._path, brim_obj_names.data.frequency))
 264            )
 265        if index >= PSD.shape[0]:
 266            raise IndexError(
 267                f"index {index} out of range for PSD with shape {PSD.shape}") 
 268        # retrieve the units of the PSD and frequency
 269        PSD_units, frequency_units = await asyncio.gather(
 270            units.of_object(self._file, PSD),
 271            units.of_object(self._file, frequency)
 272        )
 273        # map index to the frequency array, considering the broadcasting rules
 274        index_frequency = (index, ...)
 275        if frequency.ndim < PSD.ndim:
 276            # given the definition of the brim file format,
 277            # if the frequency has less dimensions that PSD,
 278            # it can only be because it is the same for all the spatial position (first dimension)
 279            index_frequency = (..., )
 280        #get the spectrum and the corresponding frequency at the specified index
 281        PSD, frequency = await asyncio.gather(
 282            _async_getitem(PSD, (index,...)),
 283            _async_getitem(frequency, index_frequency)
 284        )
 285        #broadcast the frequency to match the shape of PSD if needed
 286        if frequency.ndim < PSD.ndim:
 287            frequency = np.broadcast_to(frequency, PSD.shape)
 288        return PSD, frequency, PSD_units, frequency_units
 289
 290    def get_spectrum_in_image(self, coor: tuple) -> tuple:
 291        """
 292        Retrieve a spectrum from the data group using spatial coordinates.
 293
 294        Args:
 295            coor (tuple): A tuple containing the z, y, x coordinates of the spectrum to retrieve.
 296
 297        Returns:
 298            tuple: A tuple containing the PSD, frequency, PSD_units, frequency_units for the specified coordinates. See "get_spectrum" for details.
 299        """
 300        if len(coor) != 3:
 301            raise ValueError("coor must contain 3 values for z, y, x")
 302
 303        index = int(self._spatial_map[coor])
 304        return self.get_spectrum(index)    
 305          
 306    def get_spectrum_and_all_quantities_in_image(self, ar: 'Data.AnalysisResults', coor: tuple, index_peak: int = 0):
 307        """
 308            Retrieve the spectrum and all available quantities from the analysis results at a specific spatial coordinate.
 309            TODO complete the documentation
 310        """
 311        if len(coor) != 3:
 312            raise ValueError("coor must contain 3 values for z, y, x")
 313        index = int(self._spatial_map[coor])
 314        spectrum, quantities = _gather_sync(
 315            self.get_spectrum_async(index),
 316            ar._get_all_quantities_at_index(index, index_peak)
 317        )
 318        return spectrum, quantities
 319
 320    class AnalysisResults:
 321        """
 322        Rapresents the analysis results associated with a Data object.
 323        """
 324
 325        class Quantity(Enum):
 326            """
 327            Enum representing the type of analysis results.
 328            """
 329            Shift = "Shift"
 330            Width = "Width"
 331            Amplitude = "Amplitude"
 332            Offset = "Offset"
 333            R2 = "R2"
 334            RMSE = "RMSE"
 335            Cov_matrix = "Cov_matrix"
 336
 337        class PeakType(Enum):
 338            AntiStokes = "AS"
 339            Stokes = "S"
 340            average = "avg"
 341        
 342        class FitModel(Enum):
 343            Undefined = "Undefined"
 344            Lorentzian = "Lorentzian"
 345            DHO = "DHO"
 346            Gaussian = "Gaussian"
 347            Voigt = "Voigt"
 348            Custom = "Custom"
 349
 350        def __init__(self, file: FileAbstraction, full_path: str, spatial_map, spatial_map_px_size):
 351            """
 352            Initialize the AnalysisResults object.
 353
 354            Args:
 355                file (File): The parent File object.
 356                full_path (str): path of the group storing the analysis results
 357            """
 358            self._file = file
 359            self._path = full_path
 360            # self._group = file.open_group(full_path)
 361            self._spatial_map = spatial_map
 362            self._spatial_map_px_size = spatial_map_px_size
 363
 364        def get_name(self):
 365            """
 366            Returns the name of the Analysis group.
 367            """
 368            return sync(get_object_name(self._file, self._path))
 369
 370        @classmethod
 371        def _create_new(cls, data: 'Data', index: int) -> 'Data.AnalysisResults':
 372            """
 373            Create a new AnalysisResults group.
 374
 375            Args:
 376                file (FileAbstraction): The file.
 377                index (int): The index for the new AnalysisResults group.
 378
 379            Returns:
 380                AnalysisResults: The newly created AnalysisResults object.
 381            """
 382            group_name = f"{brim_obj_names.data.analysis_results}_{index}"
 383            ar_full_path = concatenate_paths(data._path, group_name)
 384            group = sync(data._file.create_group(ar_full_path))
 385            return cls(data._file, ar_full_path, data._spatial_map, data._spatial_map_px_size)
 386
 387        def add_data(self, data_AntiStokes=None, data_Stokes=None, fit_model: 'Data.AnalysisResults.FitModel' = None):
 388            """
 389            Adds data for the analysis results for AntiStokes and Stokes peaks to the file.
 390            
 391            Args:
 392                data_AntiStokes (dict or list[dict]): A dictionary containing the analysis results for AntiStokes peaks.
 393                    In case multiple peaks were fitted, it might be a list of dictionaries with each element corresponding to a single peak.
 394                
 395                    Each dictionary may include the following keys (plus the corresponding units,  e.g. 'shift_units'):
 396                        - 'shift': The shift value.
 397                        - 'width': The width value.
 398                        - 'amplitude': The amplitude value.
 399                        - 'offset': The offset value.
 400                        - 'R2': The R-squared value.
 401                        - 'RMSE': The root mean square error value.
 402                        - 'Cov_matrix': The covariance matrix.
 403                data_Stokes (dict or list[dict]): same as `data_AntiStokes` for the Stokes peaks.
 404                fit_model (Data.AnalysisResults.FitModel, optional): The fit model used for the analysis. Defaults to None (no attribute is set).
 405
 406                Both `data_AntiStokes` and `data_Stokes` are optional, but at least one of them must be provided.
 407            """
 408
 409            ar_cls = Data.AnalysisResults
 410            ar_group = sync(self._file.open_group(self._path))
 411
 412            def add_quantity(qt: Data.AnalysisResults.Quantity, pt: Data.AnalysisResults.PeakType, data, index: int = 0):
 413                # TODO: check if the data is valid
 414                sync(self._file.create_dataset(
 415                    ar_group, ar_cls._get_quantity_name(qt, pt, index), data))
 416
 417            def add_data_pt(pt: Data.AnalysisResults.PeakType, data, index: int = 0):
 418                if 'shift' in data:
 419                    add_quantity(ar_cls.Quantity.Shift,
 420                                 pt, data['shift'], index)
 421                    if 'shift_units' in data:
 422                        self._set_units(data['shift_units'],
 423                                        ar_cls.Quantity.Shift, pt, index)
 424                if 'width' in data:
 425                    add_quantity(ar_cls.Quantity.Width,
 426                                 pt, data['width'], index)
 427                    if 'width_units' in data:
 428                        self._set_units(data['width_units'],
 429                                        ar_cls.Quantity.Width, pt, index)
 430                if 'amplitude' in data:
 431                    add_quantity(ar_cls.Quantity.Amplitude,
 432                                 pt, data['amplitude'], index)
 433                    if 'amplitude_units' in data:
 434                        self._set_units(
 435                            data['amplitude_units'], ar_cls.Quantity.Amplitude, pt, index)
 436                if 'offset' in data:
 437                    add_quantity(ar_cls.Quantity.Offset,
 438                                 pt, data['offset'], index)
 439                    if 'offset_units' in data:
 440                        self._set_units(
 441                            data['offset_units'], ar_cls.Quantity.Offset, pt, index)
 442                if 'R2' in data:
 443                    add_quantity(ar_cls.Quantity.R2, pt, data['R2'], index)
 444                    if 'R2_units' in data:
 445                        self._set_units(data['R2_units'],
 446                                        ar_cls.Quantity.R2, pt, index)
 447                if 'RMSE' in data:
 448                    add_quantity(ar_cls.Quantity.RMSE, pt, data['RMSE'], index)
 449                    if 'RMSE_units' in data:
 450                        self._set_units(data['RMSE_units'],
 451                                        ar_cls.Quantity.RMSE, pt, index)
 452                if 'Cov_matrix' in data:
 453                    add_quantity(ar_cls.Quantity.Cov_matrix,
 454                                 pt, data['Cov_matrix'], index)
 455                    if 'Cov_matrix_units' in data:
 456                        self._set_units(
 457                            data['Cov_matrix_units'], ar_cls.Quantity.Cov_matrix, pt, index)
 458
 459            if data_AntiStokes is not None:
 460                data_AntiStokes = var_to_singleton(data_AntiStokes)
 461                for i, d_as in enumerate(data_AntiStokes):
 462                    add_data_pt(ar_cls.PeakType.AntiStokes, d_as, i)
 463            if data_Stokes is not None:
 464                data_Stokes = var_to_singleton(data_Stokes)
 465                for i, d_s in enumerate(data_Stokes):
 466                    add_data_pt(ar_cls.PeakType.Stokes, d_s, i)
 467            if fit_model is not None:
 468                sync(self._file.create_attr(ar_group, 'Fit_model', fit_model.value))
 469
 470        def get_units(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0) -> str:
 471            """
 472            Retrieve the units of a specified quantity from the data file.
 473
 474            Args:
 475                qt (Quantity): The quantity for which the units are to be retrieved.
 476                pt (PeakType, optional): The type of peak (e.g., Stokes or AntiStokes). Defaults to PeakType.AntiStokes.
 477                index (int, optional): The index of the quantity in case multiple quantities exist. Defaults to 0.
 478
 479            Returns:
 480                str: The units of the specified quantity as a string.
 481            """
 482            dt_name = Data.AnalysisResults._get_quantity_name(qt, pt, index)
 483            full_path = concatenate_paths(self._path, dt_name)
 484            return sync(units.of_object(self._file, full_path))
 485
 486        def _set_units(self, un: str, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0) -> str:
 487            """
 488            Set the units of a specified quantity.
 489
 490            Args:
 491                un (str): The units to be set.
 492                qt (Quantity): The quantity for which the units are to be set.
 493                pt (PeakType, optional): The type of peak (e.g., Stokes or AntiStokes). Defaults to PeakType.AntiStokes.
 494                index (int, optional): The index of the quantity in case multiple quantities exist. Defaults to 0.
 495
 496            Returns:
 497                str: The units of the specified quantity as a string.
 498            """
 499            dt_name = Data.AnalysisResults._get_quantity_name(qt, pt, index)
 500            full_path = concatenate_paths(self._path, dt_name)
 501            return units.add_to_object(self._file, full_path, un)
 502        
 503        @property
 504        def fit_model(self) -> 'Data.AnalysisResults.FitModel':
 505            """
 506            Retrieve the fit model used for the analysis.
 507
 508            Returns:
 509                Data.AnalysisResults.FitModel: The fit model used for the analysis.
 510            """
 511            if not hasattr(self, '_fit_model'):
 512                try:
 513                    fit_model_str = sync(self._file.get_attr(self._path, 'Fit_model'))
 514                    self._fit_model = Data.AnalysisResults.FitModel(fit_model_str)
 515                except Exception as e:
 516                    if isinstance(e, ValueError):
 517                        warnings.warn(
 518                            f"Unknown fit model '{fit_model_str}' found in the file.")
 519                    self._fit_model = Data.AnalysisResults.FitModel.Undefined        
 520            return self._fit_model
 521
 522        def save_image_to_OMETiff(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0, filename: str = None) -> str:
 523            """
 524            Saves the image corresponding to the specified quantity and index to an OMETiff file.
 525
 526            Args:
 527                qt (Quantity): The quantity to retrieve the image for (e.g. shift).
 528                pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes).
 529                index (int, optional): The index of the data to retrieve, if multiple are present (default is 0).
 530                filename (str, optional): The name of the file to save the image to. If None, a default name will be used.
 531
 532            Returns:
 533                str: The path to the saved OMETiff file.
 534            """
 535            try:
 536                import tifffile
 537            except ImportError:
 538                raise ModuleNotFoundError(
 539                    "The tifffile module is required for saving to OME-Tiff. Please install it using 'pip install tifffile'.")
 540            
 541            if filename is None:
 542                filename = f"{qt.value}_{pt.value}_{index}.ome.tif"
 543            if not filename.endswith('.ome.tif'):
 544                filename += '.ome.tif'
 545            img, px_size = self.get_image(qt, pt, index)
 546            if img.ndim > 3:
 547                raise NotImplementedError(
 548                    "Saving images with more than 3 dimensions is not supported yet.")
 549            with tifffile.TiffWriter(filename, bigtiff=True) as tif:
 550                metadata = {
 551                    'axes': 'ZYX',
 552                    'PhysicalSizeX': px_size[2].value,
 553                    'PhysicalSizeXUnit': px_size[2].units,
 554                    'PhysicalSizeY': px_size[1].value,
 555                    'PhysicalSizeYUnit': px_size[1].units,
 556                    'PhysicalSizeZ': px_size[0].value,
 557                    'PhysicalSizeZUnit': px_size[0].units,
 558                }
 559                tif.write(img, metadata=metadata)
 560            return filename
 561
 562        def get_image(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0) -> tuple:
 563            """
 564            Retrieves an image (spatial map) based on the specified quantity, peak type, and index.
 565
 566            Args:
 567                qt (Quantity): The quantity to retrieve the image for (e.g. shift).
 568                pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes).
 569                index (int, optional): The index of the data to retrieve, if multiple are present (default is 0).
 570
 571            Returns:
 572                A tuple containing the image corresponding to the specified quantity and index and the corresponding pixel size.
 573                The image is a 3D dataset where the dimensions are z, y, x.
 574                If there are additional parameters, more dimensions are added in the order z, y, x, par1, par2, ...
 575                The pixel size is a tuple of 3 Metadata.Item in the order z, y, x.
 576            """
 577            pt_type = Data.AnalysisResults.PeakType
 578            data = None
 579            if pt == pt_type.average:
 580                peaks = self.list_existing_peak_types(index)
 581                match len(peaks):
 582                    case 0:
 583                        raise ValueError(
 584                            "No peaks found for the specified index. Cannot compute average.")
 585                    case 1:
 586                        data = np.array(sync(self._get_quantity(qt, peaks[0], index)))
 587                    case 2:
 588                        data1, data2 = _gather_sync(
 589                            self._get_quantity(qt, peaks[0], index),
 590                            self._get_quantity(qt, peaks[1], index)
 591                            )
 592                        data = (np.abs(data1) + np.abs(data2))/2
 593            else:
 594                data = np.array(sync(self._get_quantity(qt, pt, index)))
 595            sm = np.array(self._spatial_map)
 596            img = data[sm, ...]
 597            img[sm<0, ...] = np.nan  # set invalid pixels to NaN
 598            return img, self._spatial_map_px_size
 599        def get_quantity_at_pixel(self, coord: tuple, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0):
 600            """
 601            Synchronous wrapper for `get_quantity_at_pixel_async` (see doc for `brimfile.data.Data.AnalysisResults.get_quantity_at_pixel_async`)
 602            """
 603            return sync(self.get_quantity_at_pixel_async(coord, qt, pt, index))
 604        async def get_quantity_at_pixel_async(self, coord: tuple, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0):
 605            """
 606            Retrieves the specified quantity in the image at coord, based on the peak type and index.
 607
 608            Args:
 609                coord (tuple): A tuple of 3 elements corresponding to the z, y, x coordinate in the image
 610                qt (Quantity): The quantity to retrieve the image for (e.g. shift).
 611                pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes).
 612                index (int, optional): The index of the data to retrieve, if multiple peaks are present (default is 0).
 613
 614            Returns:
 615                The requested quantity, which is a scalar or a multidimensional array (depending on whether there are additional parameters in the current Data group)
 616            """
 617            if len(coord) != 3:
 618                raise ValueError(
 619                    "'coord' must have 3 elements corresponding to z, y, x")
 620            i = self._spatial_map[*coord]
 621            assert i.size == 1
 622            if i<0:
 623                return np.nan  # invalid pixel
 624            i = int(i)
 625
 626            pt_type = Data.AnalysisResults.PeakType
 627            value = None
 628            if pt == pt_type.average:
 629                value = None
 630                peaks = await self.list_existing_peak_types_async(index)
 631                match len(peaks):
 632                    case 0:
 633                        raise ValueError(
 634                            "No peaks found for the specified index. Cannot compute average.")
 635                    case 1:
 636                        data = await self._get_quantity(qt, peaks[0], index)
 637                        value = await _async_getitem(data, (i, ...))
 638                    case 2:
 639                        data_p0, data_p1 = await asyncio.gather(
 640                            self._get_quantity(qt, peaks[0], index),
 641                            self._get_quantity(qt, peaks[1], index)
 642                        )
 643                        value1, value2 = await asyncio.gather(
 644                            _async_getitem(data_p0, (i, ...)),
 645                            _async_getitem(data_p1, (i, ...))
 646                        )
 647                        value = (np.abs(value1) + np.abs(value2))/2
 648            else:
 649                data = await self._get_quantity(qt, pt, index)
 650                value = await _async_getitem(data, (i, ...))
 651            return value
 652        def get_all_quantities_in_image(self, coor: tuple, index_peak: int = 0) -> dict:
 653            """
 654            Retrieve all available quantities at a specific spatial coordinate.
 655            see `brimfile.data.Data.AnalysisResults._get_all_quantities_at_index` for more details
 656            TODO complete the documentation
 657            """
 658            if len(coor) != 3:
 659                raise ValueError("coor must contain 3 values for z, y, x")
 660            index = int(self._spatial_map[coor])
 661            return sync(self._get_all_quantities_at_index(index, index_peak))
 662        async def _get_all_quantities_at_index(self, index: int, index_peak: int = 0) -> dict:
 663            """
 664            Retrieve all available quantities for a specific spatial index.
 665            Args:
 666                index (int): The spatial index to retrieve quantities for.
 667                index_peak (int, optional): The index of the data to retrieve, if multiple peaks are present (default is 0).
 668            Returns:
 669                dict: A dictionary of Metadata.Item in the form `result[quantity.name][peak.name] = bls.Metadata.Item(value, units)`
 670            """
 671            async def _get_existing_quantity_at_index_async(self,  pt: Data.AnalysisResults.PeakType = Data.AnalysisResults.PeakType.AntiStokes):
 672                as_cls = Data.AnalysisResults
 673                qts_ls = ()
 674                dts_ls = ()
 675
 676                qts = [qt for qt in as_cls.Quantity]
 677                coros = [self._file.open_dataset(concatenate_paths(self._path, as_cls._get_quantity_name(qt, pt, index_peak))) for qt in qts]
 678                
 679                # open the datasets asynchronously, excluding those that do not exist
 680                opened_dts = await asyncio.gather(*coros, return_exceptions=True)
 681                for i, opened_qt in enumerate(opened_dts):
 682                    if not isinstance(opened_qt, Exception):
 683                        qts_ls += (qts[i],)
 684                        dts_ls += (opened_dts[i],)
 685                # get the values at the specified index
 686                coros_values = [_async_getitem(dt, (index, ...)) for dt in dts_ls]
 687                coros_units = [units.of_object(self._file, dt) for dt in dts_ls]
 688                ret_ls = await asyncio.gather(*coros_values, *coros_units)
 689                n = len(coros_values)
 690                value_ls = [Metadata.Item(ret_ls[i], ret_ls[n+i]) for i in range(n)]
 691                return qts_ls, value_ls
 692            antiStokes, stokes = await asyncio.gather(
 693                _get_existing_quantity_at_index_async(self, Data.AnalysisResults.PeakType.AntiStokes),
 694                _get_existing_quantity_at_index_async(self, Data.AnalysisResults.PeakType.Stokes)
 695            )
 696            res = {}
 697            # combine the results, including the average
 698            for qt in (set(antiStokes[0]) | set(stokes[0])):
 699                res[qt.name] = {}
 700                pts = ()
 701                #Stokes
 702                if qt in stokes[0]:
 703                    res[qt.name][Data.AnalysisResults.PeakType.Stokes.name] = stokes[1][stokes[0].index(qt)]
 704                    pts += (Data.AnalysisResults.PeakType.Stokes,)
 705                #AntiStokes
 706                if qt in antiStokes[0]:
 707                    res[qt.name][Data.AnalysisResults.PeakType.AntiStokes.name] = antiStokes[1][antiStokes[0].index(qt)]
 708                    pts += (Data.AnalysisResults.PeakType.AntiStokes,)
 709                #average getting the units of the first peak
 710                res[qt.name][Data.AnalysisResults.PeakType.average.name] = Metadata.Item(
 711                    np.mean([np.abs(res[qt.name][pt.name].value) for pt in pts]), 
 712                    res[qt.name][pts[0].name].units
 713                    )
 714                if not all(res[qt.name][pt.name].units == res[qt.name][pts[0].name].units for pt in pts):
 715                    warnings.warn(f"The units of {pts} are not consistent.")
 716            return res
 717
 718        @classmethod
 719        def _get_quantity_name(cls, qt: Quantity, pt: PeakType, index: int) -> str:
 720            """
 721            Returns the name of the dataset correponding to the specific Quantity, PeakType and index
 722
 723            Args:
 724                qt (Quantity)   
 725                pt (PeakType)  
 726                intex (int): in case of multiple peaks fitted, the index of the peak to consider       
 727            """
 728            if not pt in (cls.PeakType.AntiStokes, cls.PeakType.Stokes):
 729                raise ValueError("pt has to be either Stokes or AntiStokes")
 730            if qt == cls.Quantity.R2 or qt == cls.Quantity.RMSE or qt == cls.Quantity.Cov_matrix:
 731                name = f"Fit_error_{str(pt.value)}_{index}/{str(qt.value)}"
 732            else:
 733                name = f"{str(qt.value)}_{str(pt.value)}_{index}"
 734            return name
 735
 736        async def _get_quantity(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0):
 737            """
 738            Retrieve a specific quantity dataset from the file.
 739
 740            Args:
 741                qt (Quantity): The type of quantity to retrieve.
 742                pt (PeakType, optional): The peak type to consider (default is PeakType.AntiStokes).
 743                index (int, optional): The index of the quantity if multiple peaks are available (default is 0).
 744
 745            Returns:
 746                The dataset corresponding to the specified quantity, as stored in the file.
 747
 748            """
 749
 750            dt_name = Data.AnalysisResults._get_quantity_name(qt, pt, index)
 751            full_path = concatenate_paths(self._path, dt_name)
 752            return await self._file.open_dataset(full_path)
 753
 754        def list_existing_peak_types(self, index: int = 0) -> tuple:
 755            """
 756            Synchronous wrapper for `list_existing_peak_types_async` (see doc for `brimfile.data.Data.AnalysisResults.list_existing_peak_types_async`)
 757            """
 758            return sync(self.list_existing_peak_types_async(index)) 
 759        async def list_existing_peak_types_async(self, index: int = 0) -> tuple:
 760            """
 761            Returns a tuple of existing peak types (Stokes and/or AntiStokes) for the specified index.
 762            Args:
 763                index (int, optional): The index of the peak to check (in case of multi-peak fit). Defaults to 0.
 764            Returns:
 765                tuple: A tuple containing `PeakType` members (`Stokes`, `AntiStokes`) that exist for the given index.
 766            """
 767
 768            as_cls = Data.AnalysisResults
 769            shift_s_name = as_cls._get_quantity_name(
 770                as_cls.Quantity.Shift, as_cls.PeakType.Stokes, index)
 771            shift_as_name = as_cls._get_quantity_name(
 772                as_cls.Quantity.Shift, as_cls.PeakType.AntiStokes, index)
 773            ls = ()
 774            coro_as_exists = self._file.object_exists(concatenate_paths(self._path, shift_as_name))
 775            coro_s_exists = self._file.object_exists(concatenate_paths(self._path, shift_s_name))
 776            as_exists, s_exists = await asyncio.gather(coro_as_exists, coro_s_exists)
 777            if as_exists:
 778                ls += (as_cls.PeakType.AntiStokes,)
 779            if s_exists:
 780                ls += (as_cls.PeakType.Stokes,)
 781            return ls
 782
 783        def list_existing_quantities(self,  pt: PeakType = PeakType.AntiStokes, index: int = 0) -> tuple:
 784            """
 785            Synchronous wrapper for `list_existing_quantities_async` (see doc for `brimfile.data.Data.AnalysisResults.list_existing_quantities_async`)
 786            """
 787            return sync(self.list_existing_quantities_async(pt, index))
 788        async def list_existing_quantities_async(self,  pt: PeakType = PeakType.AntiStokes, index: int = 0) -> tuple:
 789            """
 790            Returns a tuple of existing quantities for the specified index.
 791            Args:
 792                index (int, optional): The index of the peak to check (in case of multi-peak fit). Defaults to 0.
 793            Returns:
 794                tuple: A tuple containing `Quantity` members that exist for the given index.
 795            """
 796            as_cls = Data.AnalysisResults
 797            ls = ()
 798
 799            qts = [qt for qt in as_cls.Quantity]
 800            coros = [self._file.object_exists(concatenate_paths(self._path, as_cls._get_quantity_name(qt, pt, index))) for qt in qts]
 801            
 802            qt_exists = await asyncio.gather(*coros)
 803            for i, exists in enumerate(qt_exists):
 804                if exists:
 805                    ls += (qts[i],)
 806            return ls
 807
 808    def get_metadata(self):
 809        """
 810        Returns the metadata associated with the current Data group
 811        Note that this contains both the general metadata stored in the file (which might be redifined by the specific data group)
 812        and the ones specific for this data group
 813        """
 814        return Metadata(self._file, self._path)
 815
 816    def get_num_parameters(self) -> tuple:
 817        """
 818        Retrieves the number of parameters
 819
 820        Returns:
 821            tuple: The shape of the parameters if they exist, otherwise an empty tuple.
 822        """
 823        pars, _ = self.get_parameters()
 824        return pars.shape if pars is not None else ()
 825
 826    def get_parameters(self) -> list:
 827        """
 828        Retrieves the parameters  and their associated names.
 829
 830        If PSD.ndims > 2, the parameters are stored in a separate dataset.
 831
 832        Returns:
 833            list: A tuple containing the parameters and their names if there are any, otherwise None.
 834        """
 835        pars_full_path = concatenate_paths(
 836            self._path, brim_obj_names.data.parameters)
 837        if sync(self._file.object_exists(pars_full_path)):
 838            pars = sync(self._file.open_dataset(pars_full_path))
 839            pars_names = sync(self._file.get_attr(pars, 'Name'))
 840            return (pars, pars_names)
 841        return (None, None)
 842
 843    def create_analysis_results_group(self, data_AntiStokes, data_Stokes=None, index: int = None, name: str = None, fit_model: 'Data.AnalysisResults.FitModel' = None) -> AnalysisResults:
 844        """
 845        Adds a new AnalysisResults entry to the current data group.
 846        Parameters:
 847            data_AntiStokes (dict or list[dict]): contains the same elements as the ones in `AnalysisResults.add_data`,
 848                but all the quantities (i.d. 'shift', 'width', etc.) are 3D, corresponding to the spatial positions (z, y, x).
 849            data_Stokes (dict or list[dict]): same as data_AntiStokes for the Stokes peaks.
 850            index (int, optional): The index for the new data entry. If None, the next available index is used. Defaults to None.
 851            name (str, optional): The name for the new Analysis group. Defaults to None.
 852            fit_model (Data.AnalysisResults.FitModel, optional): The fit model used for the analysis. Defaults to None (no attribute is set).
 853        Returns:
 854            AnalysisResults: The newly created AnalysisResults object.
 855        Raises:
 856            IndexError: If the specified index already exists in the dataset.
 857            ValueError: If any of the data provided is not valid or consistent
 858        """
 859        def flatten_data(data: dict):
 860            if data is None:
 861                return None
 862            data = var_to_singleton(data)
 863            out_data = []
 864            for dn in data:
 865                for k in dn.keys():
 866                    if not k.endswith('_units'):
 867                        d = dn[k]
 868                        if d.ndim != 3 or d.shape != self._spatial_map.shape:
 869                            raise ValueError(
 870                                f"'{k}' must have 3 dimensions (z, y, x) and same shape as the spatial map ({self._spatial_map.shape})")
 871                        dn[k] = np.reshape(d, -1)  # flatten the data
 872                out_data.append(dn)
 873            return out_data
 874        data_AntiStokes = flatten_data(data_AntiStokes)
 875        data_Stokes = flatten_data(data_Stokes)
 876        return self.create_analysis_results_group_raw(data_AntiStokes, data_Stokes, index, name, fit_model=fit_model)
 877
 878    def create_analysis_results_group_raw(self, data_AntiStokes, data_Stokes=None, index: int = None, name: str = None, fit_model: 'Data.AnalysisResults.FitModel' = None) -> AnalysisResults:
 879        """
 880        Adds a new AnalysisResults entry to the current data group.
 881        Parameters:
 882            data_AntiStokes (dict or list[dict]): see documentation for AnalysisResults.add_data
 883            data_Stokes (dict or list[dict]): same as data_AntiStokes for the Stokes peaks.
 884            index (int, optional): The index for the new data entry. If None, the next available index is used. Defaults to None.
 885            name (str, optional): The name for the new Analysis group. Defaults to None.
 886            fit_model (Data.AnalysisResults.FitModel, optional): The fit model used for the analysis. Defaults to None (no attribute is set).
 887        Returns:
 888            AnalysisResults: The newly created AnalysisResults object.
 889        Raises:
 890            IndexError: If the specified index already exists in the dataset.
 891            ValueError: If any of the data provided is not valid or consistent
 892        """
 893        if index is not None:
 894            try:
 895                self.get_analysis_results(index)
 896            except IndexError:
 897                pass
 898            else:
 899                # If the group already exists, raise an error
 900                raise IndexError(
 901                    f"Analysis {index} already exists in {self._path}")
 902        else:
 903            ar_groups = self.list_AnalysisResults()
 904            indices = [ar['index'] for ar in ar_groups]
 905            indices.sort()
 906            index = indices[-1] + 1 if indices else 0  # Next available index
 907
 908        ar = Data.AnalysisResults._create_new(self, index)
 909        if name is not None:
 910            set_object_name(self._file, ar._path, name)
 911        ar.add_data(data_AntiStokes, data_Stokes, fit_model=fit_model)
 912
 913        return ar
 914
 915    def list_AnalysisResults(self, retrieve_custom_name=False) -> list:
 916        """
 917        List all AnalysisResults groups in the current data group. The list is ordered by index.
 918
 919        Returns:
 920            list: A list of dictionaries, each containing:
 921                - 'name' (str): The name of the AnalysisResults group.
 922                - 'index' (int): The index extracted from the group name.
 923                - 'custom_name' (str, optional): if retrieve_custom_name==True, it contains the name of the AnalysisResults group as returned from utils.get_object_name.
 924        """
 925
 926        analysis_results_groups = []
 927
 928        matched_objs = list_objects_matching_pattern(
 929            self._file, self._group, brim_obj_names.data.analysis_results + r"_(\d+)$")
 930        async def _make_dict_item(matched_obj, retrieve_custom_name):
 931            name = matched_obj[0]
 932            index = int(matched_obj[1])
 933            curr_obj_dict = {'name': name, 'index': index}
 934            if retrieve_custom_name:
 935                ar_path = concatenate_paths(self._path, name)
 936                custom_name = await get_object_name(self._file, ar_path)
 937                curr_obj_dict['custom_name'] = custom_name
 938            return curr_obj_dict
 939        coros = [_make_dict_item(matched_obj, retrieve_custom_name) for matched_obj in matched_objs]
 940        dicts = _gather_sync(*coros)
 941        for dict_item in dicts:
 942            analysis_results_groups.append(dict_item)
 943        # Sort the data groups by index
 944        analysis_results_groups.sort(key=lambda x: x['index'])
 945
 946        return analysis_results_groups
 947
 948    def get_analysis_results(self, index: int = 0) -> AnalysisResults:
 949        """
 950        Returns the AnalysisResults at the specified index
 951
 952        Args:
 953            index (int)                
 954
 955        Raises:
 956            IndexError: If there is no analysis with the corresponding index
 957        """
 958        name = None
 959        ls = self.list_AnalysisResults()
 960        for el in ls:
 961            if el['index'] == index:
 962                name = el['name']
 963                break
 964        if name is None:
 965            raise IndexError(f"Analysis {index} not found")
 966        path = concatenate_paths(self._path, name)
 967        return Data.AnalysisResults(self._file, path, self._spatial_map, self._spatial_map_px_size)
 968
 969    def add_data(self, PSD: np.ndarray, frequency: np.ndarray, scanning: dict, freq_units='GHz', timestamp: np.ndarray = None, compression: FileAbstraction.Compression = FileAbstraction.Compression()):
 970        """
 971        Add data to the current data group.
 972
 973        This method adds the provided PSD, frequency, and scanning data to the HDF5 group 
 974        associated with this `Data` object. It validates the inputs to ensure they meet 
 975        the required specifications before adding them.
 976
 977        Args:
 978            PSD (np.ndarray): A 2D numpy array representing the Power Spectral Density (PSD) data. The last dimension contains the spectra.
 979            frequency (np.ndarray): A 1D or 2D numpy array representing the frequency data. 
 980                It must be broadcastable to the shape of the PSD array.
 981            scanning (dict): A dictionary containing scanning-related data. It may include:
 982                - 'Spatial_map' (optional): A dictionary containing (up to) 3 arrays (x, y, z) and a string (units)
 983                - 'Cartesian_visualisation' (optional): A 3D numpy array containing the association between spatial position and spectra.
 984                   It must have integer values between 0 and PSD.shape[0]-1, or -1 for invalid entries.
 985                - 'Cartesian_visualisation_pixel' (optional): A list or array of 3 float values 
 986                  representing the pixel size in the z, y, and x dimensions (unused dimensions can be set to None).
 987                - 'Cartesian_visualisation_pixel_unit' (optional): A string representing the unit of the pixel size (e.g. 'um').
 988            timestamp (np.ndarray): the timestamp associated with each spectrum.
 989                It must be a 1D array with the same length as the PSD array.
 990
 991
 992        Raises:
 993            ValueError: If any of the data provided is not valid or consistent
 994        """
 995
 996        # Check if frequency is broadcastable to PSD
 997        try:
 998            np.broadcast_shapes(tuple(frequency.shape), tuple(PSD.shape))
 999        except ValueError as e:
1000            raise ValueError(f"frequency (shape: {frequency.shape}) is not broadcastable to PSD (shape: {PSD.shape}): {e}")
1001
1002        # define the scanning_is_valid variable to check if at least one of 'Spatial_map' or 'Cartesian_visualisation'
1003        # is present in the scanning dictionary
1004        scanning_is_valid = False
1005        if 'Spatial_map' in scanning:
1006            sm = scanning['Spatial_map']
1007            size = 0
1008
1009            def check_coor(coor: str):
1010                if coor in sm:
1011                    sm[coor] = np.array(sm[coor])
1012                    size1 = sm[coor].size
1013                    if size1 != size and size != 0:
1014                        raise ValueError(
1015                            f"'{coor}' in 'Spatial_map' is invalid!")
1016                    return size1
1017            size = check_coor('x')
1018            size = check_coor('y')
1019            size = check_coor('z')
1020            if size == 0:
1021                raise ValueError(
1022                    "'Spatial_map' should contain at least one x, y or z")
1023            scanning_is_valid = True
1024        if 'Cartesian_visualisation' in scanning:
1025            cv = scanning['Cartesian_visualisation']
1026            if not isinstance(cv, np.ndarray) or cv.ndim != 3:
1027                raise ValueError(
1028                    "Cartesian_visualisation must be a 3D numpy array")
1029            if not np.issubdtype(cv.dtype, np.integer) or np.min(cv) < -1 or np.max(cv) >= PSD.shape[0]:
1030                raise ValueError(
1031                    "Cartesian_visualisation values must be integers between -1 and PSD.shape[0]-1")
1032            if 'Cartesian_visualisation_pixel' in scanning:
1033                if len(scanning['Cartesian_visualisation_pixel']) != 3:
1034                    raise ValueError(
1035                        "Cartesian_visualisation_pixel must always contain 3 values for z, y, x (set to None if not used)")
1036            else:
1037                warnings.warn(
1038                    "It is recommended to add 'Cartesian_visualisation_pixel' to the scanning dictionary, to define the pixel size")
1039            scanning_is_valid = True
1040        if not scanning_is_valid:
1041            raise ValueError("scanning is not valid")
1042
1043        if timestamp is not None:
1044            if not isinstance(timestamp, np.ndarray) or timestamp.ndim != 1 or len(timestamp) != PSD.shape[0]:
1045                raise ValueError("timestamp is not compatible with PSD")
1046
1047        # TODO: add and validate additional datasets (i.e. 'Parameters', 'Calibration_index', etc.)
1048
1049        # Add datasets to the group
1050        def determine_chunk_size(arr: np.array) -> tuple:
1051            """"
1052            Use the same heuristic as the zarr library to determine the chunk size, but without splitting the last dimension
1053            """
1054            shape = arr.shape
1055            typesize = arr.itemsize
1056            #if the array is 1D, do not chunk it
1057            if len(shape) <= 1:
1058                return (shape[0],)
1059            target_sizes = _guess_chunks.__kwdefaults__
1060            # divide the target size by the last dimension size to get the chunk size for the other dimensions
1061            target_sizes = {k: target_sizes[k] // shape[-1] 
1062                            for k in target_sizes.keys()}
1063            chunks = _guess_chunks(shape[0:-1], typesize, arr.nbytes, **target_sizes)
1064            return chunks + (shape[-1],)  # keep the last dimension size unchanged
1065        sync(self._file.create_dataset(
1066            self._group, brim_obj_names.data.PSD, data=PSD,
1067            chunk_size=determine_chunk_size(PSD), compression=compression))
1068        freq_ds = sync(self._file.create_dataset(
1069            self._group,  brim_obj_names.data.frequency, data=frequency,
1070            chunk_size=determine_chunk_size(frequency), compression=compression))
1071        units.add_to_object(self._file, freq_ds, freq_units)
1072
1073        if 'Spatial_map' in scanning:
1074            sm = scanning['Spatial_map']
1075            sm_group = sync(self._file.create_group(concatenate_paths(
1076                self._path, brim_obj_names.data.spatial_map)))
1077            if 'units' in sm:
1078                units.add_to_object(self._file, sm_group, sm['units'])
1079
1080            def add_sm_dataset(coord: str):
1081                if coord in sm:
1082                    coord_dts = sync(self._file.create_dataset(
1083                        sm_group, coord, data=sm[coord], compression=compression))
1084
1085            add_sm_dataset('x')
1086            add_sm_dataset('y')
1087            add_sm_dataset('z')
1088        if 'Cartesian_visualisation' in scanning:
1089            # convert the Cartesian_visualisation to the smallest integer type
1090            cv_arr = np_array_to_smallest_int_type(scanning['Cartesian_visualisation'])
1091            cv = sync(self._file.create_dataset(self._group, brim_obj_names.data.cartesian_visualisation,
1092                                           data=cv_arr, compression=compression))
1093            if 'Cartesian_visualisation_pixel' in scanning:
1094                sync(self._file.create_attr(
1095                    cv, 'element_size', scanning['Cartesian_visualisation_pixel']))
1096                if 'Cartesian_visualisation_pixel_unit' in scanning:
1097                    px_unit = scanning['Cartesian_visualisation_pixel_unit']
1098                else:
1099                    warnings.warn(
1100                        "No unit provided for Cartesian_visualisation_pixel, defaulting to 'um'")
1101                    px_unit = 'um'
1102                units.add_to_attribute(self._file, cv, 'element_size', px_unit)
1103
1104        self._spatial_map, self._spatial_map_px_size = self._load_spatial_mapping()
1105
1106        if timestamp is not None:
1107            sync(self._file.create_dataset(
1108                self._group, 'Timestamp', data=timestamp, compression=compression))
1109
1110    @staticmethod
1111    def list_data_groups(file: FileAbstraction, retrieve_custom_name=False) -> list:
1112        """
1113        List all data groups in the brim file. The list is ordered by index.
1114
1115        Returns:
1116            list: A list of dictionaries, each containing:
1117                - 'name' (str): The name of the data group in the file.
1118                - 'index' (int): The index extracted from the group name.
1119                - 'custom_name' (str, optional): if retrieve_custom_name==True, it contains the name of the data group as returned from utils.get_object_name.
1120        """
1121
1122        data_groups = []
1123
1124        matched_objs = list_objects_matching_pattern(
1125            file, brim_obj_names.Brillouin_base_path, brim_obj_names.data.base_group + r"_(\d+)$")
1126        
1127        async def _make_dict_item(matched_obj, retrieve_custom_name):
1128            name = matched_obj[0]
1129            index = int(matched_obj[1])
1130            curr_obj_dict = {'name': name, 'index': index}
1131            if retrieve_custom_name:
1132                path = concatenate_paths(
1133                    brim_obj_names.Brillouin_base_path, name)
1134                custom_name = await get_object_name(file, path)
1135                curr_obj_dict['custom_name'] = custom_name
1136            return curr_obj_dict
1137        
1138        coros = [_make_dict_item(matched_obj, retrieve_custom_name) for matched_obj in matched_objs]
1139        dicts = _gather_sync(*coros)
1140        for dict_item in dicts:
1141            data_groups.append(dict_item)        
1142        # Sort the data groups by index
1143        data_groups.sort(key=lambda x: x['index'])
1144
1145        return data_groups
1146
1147    @staticmethod
1148    def _get_existing_group_name(file: FileAbstraction, index: int) -> str:
1149        """
1150        Get the name of an existing data group by index.
1151
1152        Args:
1153            file (File): The parent File object.
1154            index (int): The index of the data group.
1155
1156        Returns:
1157            str: The name of the data group, or None if not found.
1158        """
1159        group_name: str = None
1160        data_groups = Data.list_data_groups(file)
1161        for dg in data_groups:
1162            if dg['index'] == index:
1163                group_name = dg['name']
1164                break
1165        return group_name
1166
1167    @classmethod
1168    def _create_new(cls, file: FileAbstraction, index: int, name: str = None) -> 'Data':
1169        """
1170        Create a new data group with the specified index.
1171
1172        Args:
1173            file (File): The parent File object.
1174            index (int): The index for the new data group.
1175            name (str, optional): The name for the new data group. Defaults to None.
1176
1177        Returns:
1178            Data: The newly created Data object.
1179        """
1180        group_name = Data._generate_group_name(index)
1181        group = sync(file.create_group(concatenate_paths(
1182            brim_obj_names.Brillouin_base_path, group_name)))
1183        if name is not None:
1184            set_object_name(file, group, name)
1185        return cls(file, concatenate_paths(brim_obj_names.Brillouin_base_path, group_name))
1186
1187    @staticmethod
1188    def _generate_group_name(index: int, n_digits: int = None) -> str:
1189        """
1190        Generate a name for a data group based on the index.
1191
1192        Args:
1193            index (int): The index for the data group.
1194            n_digits (int, optional): The number of digits to pad the index with. If None no padding is applied. Defaults to None.
1195
1196        Returns:
1197            str: The generated group name.
1198
1199        Raises:
1200            ValueError: If the index is negative.
1201        """
1202        if index < 0:
1203            raise ValueError("index must be positive")
1204        num = str(index)
1205        if n_digits is not None:
1206            num = num.zfill(n_digits)
1207        return f"{brim_obj_names.data.base_group}_{num}"

Represents a data group within the brim file.

Data(file: brimfile.file_abstraction.FileAbstraction, path: str)
27    def __init__(self, file: FileAbstraction, path: str):
28        """
29        Initialize the Data object. This constructor should not be called directly.
30
31        Args:
32            file (File): The parent File object.
33            path (str): The path to the data group within the file.
34        """
35        self._file = file
36        self._path = path
37        self._group = sync(file.open_group(path))
38
39        self._spatial_map, self._spatial_map_px_size = self._load_spatial_mapping()

Initialize the Data object. This constructor should not be called directly.

Arguments:
  • file (File): The parent File object.
  • path (str): The path to the data group within the file.
def get_name(self):
41    def get_name(self):
42        """
43        Returns the name of the data group.
44        """
45        return sync(get_object_name(self._file, self._path))

Returns the name of the data group.

def get_index(self):
47    def get_index(self):
48        """
49        Returns the index of the data group.
50        """
51        return int(self._path.split('/')[-1].split('_')[-1])

Returns the index of the data group.

def get_PSD(self) -> tuple:
170    def get_PSD(self) -> tuple:
171        """
172        LOW LEVEL FUNCTION
173
174        Retrieve the Power Spectral Density (PSD) and frequency from the current data group.
175        Note: this function exposes the internals of the brim file and thus the interface might change in future versions.
176        Use only if more specialized functions are not working for your application!
177        Returns:
178            tuple: (PSD, frequency, PSD_units, frequency_units)
179                - PSD: A 2D (or more) numpy array containing all the spectra (see [specs](https://github.com/prevedel-lab/Brillouin-standard-file/blob/main/docs/brim_file_specs.md) for more details).
180                - frequency: A numpy array representing the frequency data (see [specs](https://github.com/prevedel-lab/Brillouin-standard-file/blob/main/docs/brim_file_specs.md) for more details).
181                - PSD_units: The units of the PSD.
182                - frequency_units: The units of the frequency.
183        """
184        PSD, frequency = _gather_sync(
185            self._file.open_dataset(concatenate_paths(
186                self._path, brim_obj_names.data.PSD)),
187            self._file.open_dataset(concatenate_paths(
188                self._path, brim_obj_names.data.frequency))
189        )
190        # retrieve the units of the PSD and frequency
191        PSD_units, frequency_units = _gather_sync(
192            units.of_object(self._file, PSD),
193            units.of_object(self._file, frequency)
194        )
195
196        return PSD, frequency, PSD_units, frequency_units

LOW LEVEL FUNCTION

Retrieve the Power Spectral Density (PSD) and frequency from the current data group. Note: this function exposes the internals of the brim file and thus the interface might change in future versions. Use only if more specialized functions are not working for your application!

Returns:

tuple: (PSD, frequency, PSD_units, frequency_units) - PSD: A 2D (or more) numpy array containing all the spectra (see specs for more details). - frequency: A numpy array representing the frequency data (see specs for more details). - PSD_units: The units of the PSD. - frequency_units: The units of the frequency.

def get_PSD_as_spatial_map(self) -> tuple:
198    def get_PSD_as_spatial_map(self) -> tuple:
199        """
200        Retrieve the Power Spectral Density (PSD) as a spatial map and the frequency from the current data group.
201        Returns:
202            tuple: (PSD, frequency, PSD_units, frequency_units)
203                - PSD: A 4D (or more) numpy array containing all the spectra. Dimensions are z, y, x, [parameters], spectrum.
204                - frequency: A numpy array representing the frequency data, which can be broadcastable to PSD.
205                - PSD_units: The units of the PSD.
206                - frequency_units: The units of the frequency.
207        """
208        PSD, frequency = _gather_sync(
209            self._file.open_dataset(concatenate_paths(
210                self._path, brim_obj_names.data.PSD)),        
211            self._file.open_dataset(concatenate_paths(
212                self._path, brim_obj_names.data.frequency))
213            )        
214        # retrieve the units of the PSD and frequency
215        PSD_units, frequency_units = _gather_sync(
216            units.of_object(self._file, PSD),
217            units.of_object(self._file, frequency)
218        )
219
220        # ensure PSD and frequency are numpy arrays
221        PSD = np.array(PSD)  
222        frequency = np.array(frequency)  # ensure it's a numpy array
223        
224        #if the frequency is not the same for all spectra, broadcast it to match the shape of PSD
225        if frequency.ndim > 1:
226            frequency = np.broadcast_to(frequency, PSD.shape)
227        
228        sm = np.array(self._spatial_map)
229        # reshape the PSD to have the spatial dimensions first      
230        PSD = PSD[sm, ...]
231        # reshape the frequency pnly if it is not the same for all spectra
232        if frequency.ndim > 1:
233            frequency = frequency[sm, ...]
234
235        return PSD, frequency, PSD_units, frequency_units

Retrieve the Power Spectral Density (PSD) as a spatial map and the frequency from the current data group.

Returns:

tuple: (PSD, frequency, PSD_units, frequency_units) - PSD: A 4D (or more) numpy array containing all the spectra. Dimensions are z, y, x, [parameters], spectrum. - frequency: A numpy array representing the frequency data, which can be broadcastable to PSD. - PSD_units: The units of the PSD. - frequency_units: The units of the frequency.

def get_spectrum(self, index: int) -> tuple:
237    def get_spectrum(self, index: int) -> tuple:
238        """
239        Synchronous wrapper for `get_spectrum_async` (see doc for `brimfile.data.Data.get_spectrum_async`)
240        """
241        return sync(self.get_spectrum_async(index))

Synchronous wrapper for get_spectrum_async (see doc for brimfile.data.Data.get_spectrum_async)

async def get_spectrum_async(self, index: int) -> tuple:
242    async def get_spectrum_async(self, index: int) -> tuple:
243        """
244        Retrieve a spectrum from the data group.
245
246        Args:
247            index (int): The index of the spectrum to retrieve.
248
249        Returns:
250            tuple: (PSD, frequency, PSD_units, frequency_units) for the specified index. 
251                    PSD can be 1D or more (if there are additional parameters);
252                    frequency has the same size as PSD
253        Raises:
254            IndexError: If the index is out of range for the PSD dataset.
255        """
256        # index = -1 corresponds to no spectrum
257        if index < 0:
258            return None, None, None, None
259        PSD, frequency = await asyncio.gather(
260            self._file.open_dataset(concatenate_paths(
261                self._path, brim_obj_names.data.PSD)),                       
262            self._file.open_dataset(concatenate_paths(
263                self._path, brim_obj_names.data.frequency))
264            )
265        if index >= PSD.shape[0]:
266            raise IndexError(
267                f"index {index} out of range for PSD with shape {PSD.shape}") 
268        # retrieve the units of the PSD and frequency
269        PSD_units, frequency_units = await asyncio.gather(
270            units.of_object(self._file, PSD),
271            units.of_object(self._file, frequency)
272        )
273        # map index to the frequency array, considering the broadcasting rules
274        index_frequency = (index, ...)
275        if frequency.ndim < PSD.ndim:
276            # given the definition of the brim file format,
277            # if the frequency has less dimensions that PSD,
278            # it can only be because it is the same for all the spatial position (first dimension)
279            index_frequency = (..., )
280        #get the spectrum and the corresponding frequency at the specified index
281        PSD, frequency = await asyncio.gather(
282            _async_getitem(PSD, (index,...)),
283            _async_getitem(frequency, index_frequency)
284        )
285        #broadcast the frequency to match the shape of PSD if needed
286        if frequency.ndim < PSD.ndim:
287            frequency = np.broadcast_to(frequency, PSD.shape)
288        return PSD, frequency, PSD_units, frequency_units

Retrieve a spectrum from the data group.

Arguments:
  • index (int): The index of the spectrum to retrieve.
Returns:

tuple: (PSD, frequency, PSD_units, frequency_units) for the specified index. PSD can be 1D or more (if there are additional parameters); frequency has the same size as PSD

Raises:
  • IndexError: If the index is out of range for the PSD dataset.
def get_spectrum_in_image(self, coor: tuple) -> tuple:
290    def get_spectrum_in_image(self, coor: tuple) -> tuple:
291        """
292        Retrieve a spectrum from the data group using spatial coordinates.
293
294        Args:
295            coor (tuple): A tuple containing the z, y, x coordinates of the spectrum to retrieve.
296
297        Returns:
298            tuple: A tuple containing the PSD, frequency, PSD_units, frequency_units for the specified coordinates. See "get_spectrum" for details.
299        """
300        if len(coor) != 3:
301            raise ValueError("coor must contain 3 values for z, y, x")
302
303        index = int(self._spatial_map[coor])
304        return self.get_spectrum(index)    

Retrieve a spectrum from the data group using spatial coordinates.

Arguments:
  • coor (tuple): A tuple containing the z, y, x coordinates of the spectrum to retrieve.
Returns:

tuple: A tuple containing the PSD, frequency, PSD_units, frequency_units for the specified coordinates. See "get_spectrum" for details.

def get_spectrum_and_all_quantities_in_image( self, ar: Data.AnalysisResults, coor: tuple, index_peak: int = 0):
306    def get_spectrum_and_all_quantities_in_image(self, ar: 'Data.AnalysisResults', coor: tuple, index_peak: int = 0):
307        """
308            Retrieve the spectrum and all available quantities from the analysis results at a specific spatial coordinate.
309            TODO complete the documentation
310        """
311        if len(coor) != 3:
312            raise ValueError("coor must contain 3 values for z, y, x")
313        index = int(self._spatial_map[coor])
314        spectrum, quantities = _gather_sync(
315            self.get_spectrum_async(index),
316            ar._get_all_quantities_at_index(index, index_peak)
317        )
318        return spectrum, quantities

Retrieve the spectrum and all available quantities from the analysis results at a specific spatial coordinate. TODO complete the documentation

def get_metadata(self):
808    def get_metadata(self):
809        """
810        Returns the metadata associated with the current Data group
811        Note that this contains both the general metadata stored in the file (which might be redifined by the specific data group)
812        and the ones specific for this data group
813        """
814        return Metadata(self._file, self._path)

Returns the metadata associated with the current Data group Note that this contains both the general metadata stored in the file (which might be redifined by the specific data group) and the ones specific for this data group

def get_num_parameters(self) -> tuple:
816    def get_num_parameters(self) -> tuple:
817        """
818        Retrieves the number of parameters
819
820        Returns:
821            tuple: The shape of the parameters if they exist, otherwise an empty tuple.
822        """
823        pars, _ = self.get_parameters()
824        return pars.shape if pars is not None else ()

Retrieves the number of parameters

Returns:

tuple: The shape of the parameters if they exist, otherwise an empty tuple.

def get_parameters(self) -> list:
826    def get_parameters(self) -> list:
827        """
828        Retrieves the parameters  and their associated names.
829
830        If PSD.ndims > 2, the parameters are stored in a separate dataset.
831
832        Returns:
833            list: A tuple containing the parameters and their names if there are any, otherwise None.
834        """
835        pars_full_path = concatenate_paths(
836            self._path, brim_obj_names.data.parameters)
837        if sync(self._file.object_exists(pars_full_path)):
838            pars = sync(self._file.open_dataset(pars_full_path))
839            pars_names = sync(self._file.get_attr(pars, 'Name'))
840            return (pars, pars_names)
841        return (None, None)

Retrieves the parameters and their associated names.

If PSD.ndims > 2, the parameters are stored in a separate dataset.

Returns:

list: A tuple containing the parameters and their names if there are any, otherwise None.

def create_analysis_results_group( self, data_AntiStokes, data_Stokes=None, index: int = None, name: str = None, fit_model: Data.AnalysisResults.FitModel = None) -> Data.AnalysisResults:
843    def create_analysis_results_group(self, data_AntiStokes, data_Stokes=None, index: int = None, name: str = None, fit_model: 'Data.AnalysisResults.FitModel' = None) -> AnalysisResults:
844        """
845        Adds a new AnalysisResults entry to the current data group.
846        Parameters:
847            data_AntiStokes (dict or list[dict]): contains the same elements as the ones in `AnalysisResults.add_data`,
848                but all the quantities (i.d. 'shift', 'width', etc.) are 3D, corresponding to the spatial positions (z, y, x).
849            data_Stokes (dict or list[dict]): same as data_AntiStokes for the Stokes peaks.
850            index (int, optional): The index for the new data entry. If None, the next available index is used. Defaults to None.
851            name (str, optional): The name for the new Analysis group. Defaults to None.
852            fit_model (Data.AnalysisResults.FitModel, optional): The fit model used for the analysis. Defaults to None (no attribute is set).
853        Returns:
854            AnalysisResults: The newly created AnalysisResults object.
855        Raises:
856            IndexError: If the specified index already exists in the dataset.
857            ValueError: If any of the data provided is not valid or consistent
858        """
859        def flatten_data(data: dict):
860            if data is None:
861                return None
862            data = var_to_singleton(data)
863            out_data = []
864            for dn in data:
865                for k in dn.keys():
866                    if not k.endswith('_units'):
867                        d = dn[k]
868                        if d.ndim != 3 or d.shape != self._spatial_map.shape:
869                            raise ValueError(
870                                f"'{k}' must have 3 dimensions (z, y, x) and same shape as the spatial map ({self._spatial_map.shape})")
871                        dn[k] = np.reshape(d, -1)  # flatten the data
872                out_data.append(dn)
873            return out_data
874        data_AntiStokes = flatten_data(data_AntiStokes)
875        data_Stokes = flatten_data(data_Stokes)
876        return self.create_analysis_results_group_raw(data_AntiStokes, data_Stokes, index, name, fit_model=fit_model)

Adds a new AnalysisResults entry to the current data group.

Arguments:
  • data_AntiStokes (dict or list[dict]): contains the same elements as the ones in AnalysisResults.add_data, but all the quantities (i.d. 'shift', 'width', etc.) are 3D, corresponding to the spatial positions (z, y, x).
  • data_Stokes (dict or list[dict]): same as data_AntiStokes for the Stokes peaks.
  • index (int, optional): The index for the new data entry. If None, the next available index is used. Defaults to None.
  • name (str, optional): The name for the new Analysis group. Defaults to None.
  • fit_model (Data.AnalysisResults.FitModel, optional): The fit model used for the analysis. Defaults to None (no attribute is set).
Returns:

AnalysisResults: The newly created AnalysisResults object.

Raises:
  • IndexError: If the specified index already exists in the dataset.
  • ValueError: If any of the data provided is not valid or consistent
def create_analysis_results_group_raw( self, data_AntiStokes, data_Stokes=None, index: int = None, name: str = None, fit_model: Data.AnalysisResults.FitModel = None) -> Data.AnalysisResults:
878    def create_analysis_results_group_raw(self, data_AntiStokes, data_Stokes=None, index: int = None, name: str = None, fit_model: 'Data.AnalysisResults.FitModel' = None) -> AnalysisResults:
879        """
880        Adds a new AnalysisResults entry to the current data group.
881        Parameters:
882            data_AntiStokes (dict or list[dict]): see documentation for AnalysisResults.add_data
883            data_Stokes (dict or list[dict]): same as data_AntiStokes for the Stokes peaks.
884            index (int, optional): The index for the new data entry. If None, the next available index is used. Defaults to None.
885            name (str, optional): The name for the new Analysis group. Defaults to None.
886            fit_model (Data.AnalysisResults.FitModel, optional): The fit model used for the analysis. Defaults to None (no attribute is set).
887        Returns:
888            AnalysisResults: The newly created AnalysisResults object.
889        Raises:
890            IndexError: If the specified index already exists in the dataset.
891            ValueError: If any of the data provided is not valid or consistent
892        """
893        if index is not None:
894            try:
895                self.get_analysis_results(index)
896            except IndexError:
897                pass
898            else:
899                # If the group already exists, raise an error
900                raise IndexError(
901                    f"Analysis {index} already exists in {self._path}")
902        else:
903            ar_groups = self.list_AnalysisResults()
904            indices = [ar['index'] for ar in ar_groups]
905            indices.sort()
906            index = indices[-1] + 1 if indices else 0  # Next available index
907
908        ar = Data.AnalysisResults._create_new(self, index)
909        if name is not None:
910            set_object_name(self._file, ar._path, name)
911        ar.add_data(data_AntiStokes, data_Stokes, fit_model=fit_model)
912
913        return ar

Adds a new AnalysisResults entry to the current data group.

Arguments:
  • data_AntiStokes (dict or list[dict]): see documentation for AnalysisResults.add_data
  • data_Stokes (dict or list[dict]): same as data_AntiStokes for the Stokes peaks.
  • index (int, optional): The index for the new data entry. If None, the next available index is used. Defaults to None.
  • name (str, optional): The name for the new Analysis group. Defaults to None.
  • fit_model (Data.AnalysisResults.FitModel, optional): The fit model used for the analysis. Defaults to None (no attribute is set).
Returns:

AnalysisResults: The newly created AnalysisResults object.

Raises:
  • IndexError: If the specified index already exists in the dataset.
  • ValueError: If any of the data provided is not valid or consistent
def list_AnalysisResults(self, retrieve_custom_name=False) -> list:
915    def list_AnalysisResults(self, retrieve_custom_name=False) -> list:
916        """
917        List all AnalysisResults groups in the current data group. The list is ordered by index.
918
919        Returns:
920            list: A list of dictionaries, each containing:
921                - 'name' (str): The name of the AnalysisResults group.
922                - 'index' (int): The index extracted from the group name.
923                - 'custom_name' (str, optional): if retrieve_custom_name==True, it contains the name of the AnalysisResults group as returned from utils.get_object_name.
924        """
925
926        analysis_results_groups = []
927
928        matched_objs = list_objects_matching_pattern(
929            self._file, self._group, brim_obj_names.data.analysis_results + r"_(\d+)$")
930        async def _make_dict_item(matched_obj, retrieve_custom_name):
931            name = matched_obj[0]
932            index = int(matched_obj[1])
933            curr_obj_dict = {'name': name, 'index': index}
934            if retrieve_custom_name:
935                ar_path = concatenate_paths(self._path, name)
936                custom_name = await get_object_name(self._file, ar_path)
937                curr_obj_dict['custom_name'] = custom_name
938            return curr_obj_dict
939        coros = [_make_dict_item(matched_obj, retrieve_custom_name) for matched_obj in matched_objs]
940        dicts = _gather_sync(*coros)
941        for dict_item in dicts:
942            analysis_results_groups.append(dict_item)
943        # Sort the data groups by index
944        analysis_results_groups.sort(key=lambda x: x['index'])
945
946        return analysis_results_groups

List all AnalysisResults groups in the current data group. The list is ordered by index.

Returns:

list: A list of dictionaries, each containing: - 'name' (str): The name of the AnalysisResults group. - 'index' (int): The index extracted from the group name. - 'custom_name' (str, optional): if retrieve_custom_name==True, it contains the name of the AnalysisResults group as returned from utils.get_object_name.

def get_analysis_results(self, index: int = 0) -> Data.AnalysisResults:
948    def get_analysis_results(self, index: int = 0) -> AnalysisResults:
949        """
950        Returns the AnalysisResults at the specified index
951
952        Args:
953            index (int)                
954
955        Raises:
956            IndexError: If there is no analysis with the corresponding index
957        """
958        name = None
959        ls = self.list_AnalysisResults()
960        for el in ls:
961            if el['index'] == index:
962                name = el['name']
963                break
964        if name is None:
965            raise IndexError(f"Analysis {index} not found")
966        path = concatenate_paths(self._path, name)
967        return Data.AnalysisResults(self._file, path, self._spatial_map, self._spatial_map_px_size)

Returns the AnalysisResults at the specified index

Arguments:
  • index (int)
Raises:
  • IndexError: If there is no analysis with the corresponding index
def add_data( self, PSD: numpy.ndarray, frequency: numpy.ndarray, scanning: dict, freq_units='GHz', timestamp: numpy.ndarray = None, compression: brimfile.file_abstraction.FileAbstraction.Compression = <brimfile.file_abstraction.FileAbstraction.Compression object>):
 969    def add_data(self, PSD: np.ndarray, frequency: np.ndarray, scanning: dict, freq_units='GHz', timestamp: np.ndarray = None, compression: FileAbstraction.Compression = FileAbstraction.Compression()):
 970        """
 971        Add data to the current data group.
 972
 973        This method adds the provided PSD, frequency, and scanning data to the HDF5 group 
 974        associated with this `Data` object. It validates the inputs to ensure they meet 
 975        the required specifications before adding them.
 976
 977        Args:
 978            PSD (np.ndarray): A 2D numpy array representing the Power Spectral Density (PSD) data. The last dimension contains the spectra.
 979            frequency (np.ndarray): A 1D or 2D numpy array representing the frequency data. 
 980                It must be broadcastable to the shape of the PSD array.
 981            scanning (dict): A dictionary containing scanning-related data. It may include:
 982                - 'Spatial_map' (optional): A dictionary containing (up to) 3 arrays (x, y, z) and a string (units)
 983                - 'Cartesian_visualisation' (optional): A 3D numpy array containing the association between spatial position and spectra.
 984                   It must have integer values between 0 and PSD.shape[0]-1, or -1 for invalid entries.
 985                - 'Cartesian_visualisation_pixel' (optional): A list or array of 3 float values 
 986                  representing the pixel size in the z, y, and x dimensions (unused dimensions can be set to None).
 987                - 'Cartesian_visualisation_pixel_unit' (optional): A string representing the unit of the pixel size (e.g. 'um').
 988            timestamp (np.ndarray): the timestamp associated with each spectrum.
 989                It must be a 1D array with the same length as the PSD array.
 990
 991
 992        Raises:
 993            ValueError: If any of the data provided is not valid or consistent
 994        """
 995
 996        # Check if frequency is broadcastable to PSD
 997        try:
 998            np.broadcast_shapes(tuple(frequency.shape), tuple(PSD.shape))
 999        except ValueError as e:
1000            raise ValueError(f"frequency (shape: {frequency.shape}) is not broadcastable to PSD (shape: {PSD.shape}): {e}")
1001
1002        # define the scanning_is_valid variable to check if at least one of 'Spatial_map' or 'Cartesian_visualisation'
1003        # is present in the scanning dictionary
1004        scanning_is_valid = False
1005        if 'Spatial_map' in scanning:
1006            sm = scanning['Spatial_map']
1007            size = 0
1008
1009            def check_coor(coor: str):
1010                if coor in sm:
1011                    sm[coor] = np.array(sm[coor])
1012                    size1 = sm[coor].size
1013                    if size1 != size and size != 0:
1014                        raise ValueError(
1015                            f"'{coor}' in 'Spatial_map' is invalid!")
1016                    return size1
1017            size = check_coor('x')
1018            size = check_coor('y')
1019            size = check_coor('z')
1020            if size == 0:
1021                raise ValueError(
1022                    "'Spatial_map' should contain at least one x, y or z")
1023            scanning_is_valid = True
1024        if 'Cartesian_visualisation' in scanning:
1025            cv = scanning['Cartesian_visualisation']
1026            if not isinstance(cv, np.ndarray) or cv.ndim != 3:
1027                raise ValueError(
1028                    "Cartesian_visualisation must be a 3D numpy array")
1029            if not np.issubdtype(cv.dtype, np.integer) or np.min(cv) < -1 or np.max(cv) >= PSD.shape[0]:
1030                raise ValueError(
1031                    "Cartesian_visualisation values must be integers between -1 and PSD.shape[0]-1")
1032            if 'Cartesian_visualisation_pixel' in scanning:
1033                if len(scanning['Cartesian_visualisation_pixel']) != 3:
1034                    raise ValueError(
1035                        "Cartesian_visualisation_pixel must always contain 3 values for z, y, x (set to None if not used)")
1036            else:
1037                warnings.warn(
1038                    "It is recommended to add 'Cartesian_visualisation_pixel' to the scanning dictionary, to define the pixel size")
1039            scanning_is_valid = True
1040        if not scanning_is_valid:
1041            raise ValueError("scanning is not valid")
1042
1043        if timestamp is not None:
1044            if not isinstance(timestamp, np.ndarray) or timestamp.ndim != 1 or len(timestamp) != PSD.shape[0]:
1045                raise ValueError("timestamp is not compatible with PSD")
1046
1047        # TODO: add and validate additional datasets (i.e. 'Parameters', 'Calibration_index', etc.)
1048
1049        # Add datasets to the group
1050        def determine_chunk_size(arr: np.array) -> tuple:
1051            """"
1052            Use the same heuristic as the zarr library to determine the chunk size, but without splitting the last dimension
1053            """
1054            shape = arr.shape
1055            typesize = arr.itemsize
1056            #if the array is 1D, do not chunk it
1057            if len(shape) <= 1:
1058                return (shape[0],)
1059            target_sizes = _guess_chunks.__kwdefaults__
1060            # divide the target size by the last dimension size to get the chunk size for the other dimensions
1061            target_sizes = {k: target_sizes[k] // shape[-1] 
1062                            for k in target_sizes.keys()}
1063            chunks = _guess_chunks(shape[0:-1], typesize, arr.nbytes, **target_sizes)
1064            return chunks + (shape[-1],)  # keep the last dimension size unchanged
1065        sync(self._file.create_dataset(
1066            self._group, brim_obj_names.data.PSD, data=PSD,
1067            chunk_size=determine_chunk_size(PSD), compression=compression))
1068        freq_ds = sync(self._file.create_dataset(
1069            self._group,  brim_obj_names.data.frequency, data=frequency,
1070            chunk_size=determine_chunk_size(frequency), compression=compression))
1071        units.add_to_object(self._file, freq_ds, freq_units)
1072
1073        if 'Spatial_map' in scanning:
1074            sm = scanning['Spatial_map']
1075            sm_group = sync(self._file.create_group(concatenate_paths(
1076                self._path, brim_obj_names.data.spatial_map)))
1077            if 'units' in sm:
1078                units.add_to_object(self._file, sm_group, sm['units'])
1079
1080            def add_sm_dataset(coord: str):
1081                if coord in sm:
1082                    coord_dts = sync(self._file.create_dataset(
1083                        sm_group, coord, data=sm[coord], compression=compression))
1084
1085            add_sm_dataset('x')
1086            add_sm_dataset('y')
1087            add_sm_dataset('z')
1088        if 'Cartesian_visualisation' in scanning:
1089            # convert the Cartesian_visualisation to the smallest integer type
1090            cv_arr = np_array_to_smallest_int_type(scanning['Cartesian_visualisation'])
1091            cv = sync(self._file.create_dataset(self._group, brim_obj_names.data.cartesian_visualisation,
1092                                           data=cv_arr, compression=compression))
1093            if 'Cartesian_visualisation_pixel' in scanning:
1094                sync(self._file.create_attr(
1095                    cv, 'element_size', scanning['Cartesian_visualisation_pixel']))
1096                if 'Cartesian_visualisation_pixel_unit' in scanning:
1097                    px_unit = scanning['Cartesian_visualisation_pixel_unit']
1098                else:
1099                    warnings.warn(
1100                        "No unit provided for Cartesian_visualisation_pixel, defaulting to 'um'")
1101                    px_unit = 'um'
1102                units.add_to_attribute(self._file, cv, 'element_size', px_unit)
1103
1104        self._spatial_map, self._spatial_map_px_size = self._load_spatial_mapping()
1105
1106        if timestamp is not None:
1107            sync(self._file.create_dataset(
1108                self._group, 'Timestamp', data=timestamp, compression=compression))

Add data to the current data group.

This method adds the provided PSD, frequency, and scanning data to the HDF5 group associated with this Data object. It validates the inputs to ensure they meet the required specifications before adding them.

Arguments:
  • PSD (np.ndarray): A 2D numpy array representing the Power Spectral Density (PSD) data. The last dimension contains the spectra.
  • frequency (np.ndarray): A 1D or 2D numpy array representing the frequency data. It must be broadcastable to the shape of the PSD array.
  • scanning (dict): A dictionary containing scanning-related data. It may include:
    • 'Spatial_map' (optional): A dictionary containing (up to) 3 arrays (x, y, z) and a string (units)
    • 'Cartesian_visualisation' (optional): A 3D numpy array containing the association between spatial position and spectra. It must have integer values between 0 and PSD.shape[0]-1, or -1 for invalid entries.
    • 'Cartesian_visualisation_pixel' (optional): A list or array of 3 float values representing the pixel size in the z, y, and x dimensions (unused dimensions can be set to None).
    • 'Cartesian_visualisation_pixel_unit' (optional): A string representing the unit of the pixel size (e.g. 'um').
  • timestamp (np.ndarray): the timestamp associated with each spectrum. It must be a 1D array with the same length as the PSD array.
Raises:
  • ValueError: If any of the data provided is not valid or consistent
@staticmethod
def list_data_groups( file: brimfile.file_abstraction.FileAbstraction, retrieve_custom_name=False) -> list:
1110    @staticmethod
1111    def list_data_groups(file: FileAbstraction, retrieve_custom_name=False) -> list:
1112        """
1113        List all data groups in the brim file. The list is ordered by index.
1114
1115        Returns:
1116            list: A list of dictionaries, each containing:
1117                - 'name' (str): The name of the data group in the file.
1118                - 'index' (int): The index extracted from the group name.
1119                - 'custom_name' (str, optional): if retrieve_custom_name==True, it contains the name of the data group as returned from utils.get_object_name.
1120        """
1121
1122        data_groups = []
1123
1124        matched_objs = list_objects_matching_pattern(
1125            file, brim_obj_names.Brillouin_base_path, brim_obj_names.data.base_group + r"_(\d+)$")
1126        
1127        async def _make_dict_item(matched_obj, retrieve_custom_name):
1128            name = matched_obj[0]
1129            index = int(matched_obj[1])
1130            curr_obj_dict = {'name': name, 'index': index}
1131            if retrieve_custom_name:
1132                path = concatenate_paths(
1133                    brim_obj_names.Brillouin_base_path, name)
1134                custom_name = await get_object_name(file, path)
1135                curr_obj_dict['custom_name'] = custom_name
1136            return curr_obj_dict
1137        
1138        coros = [_make_dict_item(matched_obj, retrieve_custom_name) for matched_obj in matched_objs]
1139        dicts = _gather_sync(*coros)
1140        for dict_item in dicts:
1141            data_groups.append(dict_item)        
1142        # Sort the data groups by index
1143        data_groups.sort(key=lambda x: x['index'])
1144
1145        return data_groups

List all data groups in the brim file. The list is ordered by index.

Returns:

list: A list of dictionaries, each containing: - 'name' (str): The name of the data group in the file. - 'index' (int): The index extracted from the group name. - 'custom_name' (str, optional): if retrieve_custom_name==True, it contains the name of the data group as returned from utils.get_object_name.

class Data.AnalysisResults:
320    class AnalysisResults:
321        """
322        Rapresents the analysis results associated with a Data object.
323        """
324
325        class Quantity(Enum):
326            """
327            Enum representing the type of analysis results.
328            """
329            Shift = "Shift"
330            Width = "Width"
331            Amplitude = "Amplitude"
332            Offset = "Offset"
333            R2 = "R2"
334            RMSE = "RMSE"
335            Cov_matrix = "Cov_matrix"
336
337        class PeakType(Enum):
338            AntiStokes = "AS"
339            Stokes = "S"
340            average = "avg"
341        
342        class FitModel(Enum):
343            Undefined = "Undefined"
344            Lorentzian = "Lorentzian"
345            DHO = "DHO"
346            Gaussian = "Gaussian"
347            Voigt = "Voigt"
348            Custom = "Custom"
349
350        def __init__(self, file: FileAbstraction, full_path: str, spatial_map, spatial_map_px_size):
351            """
352            Initialize the AnalysisResults object.
353
354            Args:
355                file (File): The parent File object.
356                full_path (str): path of the group storing the analysis results
357            """
358            self._file = file
359            self._path = full_path
360            # self._group = file.open_group(full_path)
361            self._spatial_map = spatial_map
362            self._spatial_map_px_size = spatial_map_px_size
363
364        def get_name(self):
365            """
366            Returns the name of the Analysis group.
367            """
368            return sync(get_object_name(self._file, self._path))
369
370        @classmethod
371        def _create_new(cls, data: 'Data', index: int) -> 'Data.AnalysisResults':
372            """
373            Create a new AnalysisResults group.
374
375            Args:
376                file (FileAbstraction): The file.
377                index (int): The index for the new AnalysisResults group.
378
379            Returns:
380                AnalysisResults: The newly created AnalysisResults object.
381            """
382            group_name = f"{brim_obj_names.data.analysis_results}_{index}"
383            ar_full_path = concatenate_paths(data._path, group_name)
384            group = sync(data._file.create_group(ar_full_path))
385            return cls(data._file, ar_full_path, data._spatial_map, data._spatial_map_px_size)
386
387        def add_data(self, data_AntiStokes=None, data_Stokes=None, fit_model: 'Data.AnalysisResults.FitModel' = None):
388            """
389            Adds data for the analysis results for AntiStokes and Stokes peaks to the file.
390            
391            Args:
392                data_AntiStokes (dict or list[dict]): A dictionary containing the analysis results for AntiStokes peaks.
393                    In case multiple peaks were fitted, it might be a list of dictionaries with each element corresponding to a single peak.
394                
395                    Each dictionary may include the following keys (plus the corresponding units,  e.g. 'shift_units'):
396                        - 'shift': The shift value.
397                        - 'width': The width value.
398                        - 'amplitude': The amplitude value.
399                        - 'offset': The offset value.
400                        - 'R2': The R-squared value.
401                        - 'RMSE': The root mean square error value.
402                        - 'Cov_matrix': The covariance matrix.
403                data_Stokes (dict or list[dict]): same as `data_AntiStokes` for the Stokes peaks.
404                fit_model (Data.AnalysisResults.FitModel, optional): The fit model used for the analysis. Defaults to None (no attribute is set).
405
406                Both `data_AntiStokes` and `data_Stokes` are optional, but at least one of them must be provided.
407            """
408
409            ar_cls = Data.AnalysisResults
410            ar_group = sync(self._file.open_group(self._path))
411
412            def add_quantity(qt: Data.AnalysisResults.Quantity, pt: Data.AnalysisResults.PeakType, data, index: int = 0):
413                # TODO: check if the data is valid
414                sync(self._file.create_dataset(
415                    ar_group, ar_cls._get_quantity_name(qt, pt, index), data))
416
417            def add_data_pt(pt: Data.AnalysisResults.PeakType, data, index: int = 0):
418                if 'shift' in data:
419                    add_quantity(ar_cls.Quantity.Shift,
420                                 pt, data['shift'], index)
421                    if 'shift_units' in data:
422                        self._set_units(data['shift_units'],
423                                        ar_cls.Quantity.Shift, pt, index)
424                if 'width' in data:
425                    add_quantity(ar_cls.Quantity.Width,
426                                 pt, data['width'], index)
427                    if 'width_units' in data:
428                        self._set_units(data['width_units'],
429                                        ar_cls.Quantity.Width, pt, index)
430                if 'amplitude' in data:
431                    add_quantity(ar_cls.Quantity.Amplitude,
432                                 pt, data['amplitude'], index)
433                    if 'amplitude_units' in data:
434                        self._set_units(
435                            data['amplitude_units'], ar_cls.Quantity.Amplitude, pt, index)
436                if 'offset' in data:
437                    add_quantity(ar_cls.Quantity.Offset,
438                                 pt, data['offset'], index)
439                    if 'offset_units' in data:
440                        self._set_units(
441                            data['offset_units'], ar_cls.Quantity.Offset, pt, index)
442                if 'R2' in data:
443                    add_quantity(ar_cls.Quantity.R2, pt, data['R2'], index)
444                    if 'R2_units' in data:
445                        self._set_units(data['R2_units'],
446                                        ar_cls.Quantity.R2, pt, index)
447                if 'RMSE' in data:
448                    add_quantity(ar_cls.Quantity.RMSE, pt, data['RMSE'], index)
449                    if 'RMSE_units' in data:
450                        self._set_units(data['RMSE_units'],
451                                        ar_cls.Quantity.RMSE, pt, index)
452                if 'Cov_matrix' in data:
453                    add_quantity(ar_cls.Quantity.Cov_matrix,
454                                 pt, data['Cov_matrix'], index)
455                    if 'Cov_matrix_units' in data:
456                        self._set_units(
457                            data['Cov_matrix_units'], ar_cls.Quantity.Cov_matrix, pt, index)
458
459            if data_AntiStokes is not None:
460                data_AntiStokes = var_to_singleton(data_AntiStokes)
461                for i, d_as in enumerate(data_AntiStokes):
462                    add_data_pt(ar_cls.PeakType.AntiStokes, d_as, i)
463            if data_Stokes is not None:
464                data_Stokes = var_to_singleton(data_Stokes)
465                for i, d_s in enumerate(data_Stokes):
466                    add_data_pt(ar_cls.PeakType.Stokes, d_s, i)
467            if fit_model is not None:
468                sync(self._file.create_attr(ar_group, 'Fit_model', fit_model.value))
469
470        def get_units(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0) -> str:
471            """
472            Retrieve the units of a specified quantity from the data file.
473
474            Args:
475                qt (Quantity): The quantity for which the units are to be retrieved.
476                pt (PeakType, optional): The type of peak (e.g., Stokes or AntiStokes). Defaults to PeakType.AntiStokes.
477                index (int, optional): The index of the quantity in case multiple quantities exist. Defaults to 0.
478
479            Returns:
480                str: The units of the specified quantity as a string.
481            """
482            dt_name = Data.AnalysisResults._get_quantity_name(qt, pt, index)
483            full_path = concatenate_paths(self._path, dt_name)
484            return sync(units.of_object(self._file, full_path))
485
486        def _set_units(self, un: str, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0) -> str:
487            """
488            Set the units of a specified quantity.
489
490            Args:
491                un (str): The units to be set.
492                qt (Quantity): The quantity for which the units are to be set.
493                pt (PeakType, optional): The type of peak (e.g., Stokes or AntiStokes). Defaults to PeakType.AntiStokes.
494                index (int, optional): The index of the quantity in case multiple quantities exist. Defaults to 0.
495
496            Returns:
497                str: The units of the specified quantity as a string.
498            """
499            dt_name = Data.AnalysisResults._get_quantity_name(qt, pt, index)
500            full_path = concatenate_paths(self._path, dt_name)
501            return units.add_to_object(self._file, full_path, un)
502        
503        @property
504        def fit_model(self) -> 'Data.AnalysisResults.FitModel':
505            """
506            Retrieve the fit model used for the analysis.
507
508            Returns:
509                Data.AnalysisResults.FitModel: The fit model used for the analysis.
510            """
511            if not hasattr(self, '_fit_model'):
512                try:
513                    fit_model_str = sync(self._file.get_attr(self._path, 'Fit_model'))
514                    self._fit_model = Data.AnalysisResults.FitModel(fit_model_str)
515                except Exception as e:
516                    if isinstance(e, ValueError):
517                        warnings.warn(
518                            f"Unknown fit model '{fit_model_str}' found in the file.")
519                    self._fit_model = Data.AnalysisResults.FitModel.Undefined        
520            return self._fit_model
521
522        def save_image_to_OMETiff(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0, filename: str = None) -> str:
523            """
524            Saves the image corresponding to the specified quantity and index to an OMETiff file.
525
526            Args:
527                qt (Quantity): The quantity to retrieve the image for (e.g. shift).
528                pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes).
529                index (int, optional): The index of the data to retrieve, if multiple are present (default is 0).
530                filename (str, optional): The name of the file to save the image to. If None, a default name will be used.
531
532            Returns:
533                str: The path to the saved OMETiff file.
534            """
535            try:
536                import tifffile
537            except ImportError:
538                raise ModuleNotFoundError(
539                    "The tifffile module is required for saving to OME-Tiff. Please install it using 'pip install tifffile'.")
540            
541            if filename is None:
542                filename = f"{qt.value}_{pt.value}_{index}.ome.tif"
543            if not filename.endswith('.ome.tif'):
544                filename += '.ome.tif'
545            img, px_size = self.get_image(qt, pt, index)
546            if img.ndim > 3:
547                raise NotImplementedError(
548                    "Saving images with more than 3 dimensions is not supported yet.")
549            with tifffile.TiffWriter(filename, bigtiff=True) as tif:
550                metadata = {
551                    'axes': 'ZYX',
552                    'PhysicalSizeX': px_size[2].value,
553                    'PhysicalSizeXUnit': px_size[2].units,
554                    'PhysicalSizeY': px_size[1].value,
555                    'PhysicalSizeYUnit': px_size[1].units,
556                    'PhysicalSizeZ': px_size[0].value,
557                    'PhysicalSizeZUnit': px_size[0].units,
558                }
559                tif.write(img, metadata=metadata)
560            return filename
561
562        def get_image(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0) -> tuple:
563            """
564            Retrieves an image (spatial map) based on the specified quantity, peak type, and index.
565
566            Args:
567                qt (Quantity): The quantity to retrieve the image for (e.g. shift).
568                pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes).
569                index (int, optional): The index of the data to retrieve, if multiple are present (default is 0).
570
571            Returns:
572                A tuple containing the image corresponding to the specified quantity and index and the corresponding pixel size.
573                The image is a 3D dataset where the dimensions are z, y, x.
574                If there are additional parameters, more dimensions are added in the order z, y, x, par1, par2, ...
575                The pixel size is a tuple of 3 Metadata.Item in the order z, y, x.
576            """
577            pt_type = Data.AnalysisResults.PeakType
578            data = None
579            if pt == pt_type.average:
580                peaks = self.list_existing_peak_types(index)
581                match len(peaks):
582                    case 0:
583                        raise ValueError(
584                            "No peaks found for the specified index. Cannot compute average.")
585                    case 1:
586                        data = np.array(sync(self._get_quantity(qt, peaks[0], index)))
587                    case 2:
588                        data1, data2 = _gather_sync(
589                            self._get_quantity(qt, peaks[0], index),
590                            self._get_quantity(qt, peaks[1], index)
591                            )
592                        data = (np.abs(data1) + np.abs(data2))/2
593            else:
594                data = np.array(sync(self._get_quantity(qt, pt, index)))
595            sm = np.array(self._spatial_map)
596            img = data[sm, ...]
597            img[sm<0, ...] = np.nan  # set invalid pixels to NaN
598            return img, self._spatial_map_px_size
599        def get_quantity_at_pixel(self, coord: tuple, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0):
600            """
601            Synchronous wrapper for `get_quantity_at_pixel_async` (see doc for `brimfile.data.Data.AnalysisResults.get_quantity_at_pixel_async`)
602            """
603            return sync(self.get_quantity_at_pixel_async(coord, qt, pt, index))
604        async def get_quantity_at_pixel_async(self, coord: tuple, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0):
605            """
606            Retrieves the specified quantity in the image at coord, based on the peak type and index.
607
608            Args:
609                coord (tuple): A tuple of 3 elements corresponding to the z, y, x coordinate in the image
610                qt (Quantity): The quantity to retrieve the image for (e.g. shift).
611                pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes).
612                index (int, optional): The index of the data to retrieve, if multiple peaks are present (default is 0).
613
614            Returns:
615                The requested quantity, which is a scalar or a multidimensional array (depending on whether there are additional parameters in the current Data group)
616            """
617            if len(coord) != 3:
618                raise ValueError(
619                    "'coord' must have 3 elements corresponding to z, y, x")
620            i = self._spatial_map[*coord]
621            assert i.size == 1
622            if i<0:
623                return np.nan  # invalid pixel
624            i = int(i)
625
626            pt_type = Data.AnalysisResults.PeakType
627            value = None
628            if pt == pt_type.average:
629                value = None
630                peaks = await self.list_existing_peak_types_async(index)
631                match len(peaks):
632                    case 0:
633                        raise ValueError(
634                            "No peaks found for the specified index. Cannot compute average.")
635                    case 1:
636                        data = await self._get_quantity(qt, peaks[0], index)
637                        value = await _async_getitem(data, (i, ...))
638                    case 2:
639                        data_p0, data_p1 = await asyncio.gather(
640                            self._get_quantity(qt, peaks[0], index),
641                            self._get_quantity(qt, peaks[1], index)
642                        )
643                        value1, value2 = await asyncio.gather(
644                            _async_getitem(data_p0, (i, ...)),
645                            _async_getitem(data_p1, (i, ...))
646                        )
647                        value = (np.abs(value1) + np.abs(value2))/2
648            else:
649                data = await self._get_quantity(qt, pt, index)
650                value = await _async_getitem(data, (i, ...))
651            return value
652        def get_all_quantities_in_image(self, coor: tuple, index_peak: int = 0) -> dict:
653            """
654            Retrieve all available quantities at a specific spatial coordinate.
655            see `brimfile.data.Data.AnalysisResults._get_all_quantities_at_index` for more details
656            TODO complete the documentation
657            """
658            if len(coor) != 3:
659                raise ValueError("coor must contain 3 values for z, y, x")
660            index = int(self._spatial_map[coor])
661            return sync(self._get_all_quantities_at_index(index, index_peak))
662        async def _get_all_quantities_at_index(self, index: int, index_peak: int = 0) -> dict:
663            """
664            Retrieve all available quantities for a specific spatial index.
665            Args:
666                index (int): The spatial index to retrieve quantities for.
667                index_peak (int, optional): The index of the data to retrieve, if multiple peaks are present (default is 0).
668            Returns:
669                dict: A dictionary of Metadata.Item in the form `result[quantity.name][peak.name] = bls.Metadata.Item(value, units)`
670            """
671            async def _get_existing_quantity_at_index_async(self,  pt: Data.AnalysisResults.PeakType = Data.AnalysisResults.PeakType.AntiStokes):
672                as_cls = Data.AnalysisResults
673                qts_ls = ()
674                dts_ls = ()
675
676                qts = [qt for qt in as_cls.Quantity]
677                coros = [self._file.open_dataset(concatenate_paths(self._path, as_cls._get_quantity_name(qt, pt, index_peak))) for qt in qts]
678                
679                # open the datasets asynchronously, excluding those that do not exist
680                opened_dts = await asyncio.gather(*coros, return_exceptions=True)
681                for i, opened_qt in enumerate(opened_dts):
682                    if not isinstance(opened_qt, Exception):
683                        qts_ls += (qts[i],)
684                        dts_ls += (opened_dts[i],)
685                # get the values at the specified index
686                coros_values = [_async_getitem(dt, (index, ...)) for dt in dts_ls]
687                coros_units = [units.of_object(self._file, dt) for dt in dts_ls]
688                ret_ls = await asyncio.gather(*coros_values, *coros_units)
689                n = len(coros_values)
690                value_ls = [Metadata.Item(ret_ls[i], ret_ls[n+i]) for i in range(n)]
691                return qts_ls, value_ls
692            antiStokes, stokes = await asyncio.gather(
693                _get_existing_quantity_at_index_async(self, Data.AnalysisResults.PeakType.AntiStokes),
694                _get_existing_quantity_at_index_async(self, Data.AnalysisResults.PeakType.Stokes)
695            )
696            res = {}
697            # combine the results, including the average
698            for qt in (set(antiStokes[0]) | set(stokes[0])):
699                res[qt.name] = {}
700                pts = ()
701                #Stokes
702                if qt in stokes[0]:
703                    res[qt.name][Data.AnalysisResults.PeakType.Stokes.name] = stokes[1][stokes[0].index(qt)]
704                    pts += (Data.AnalysisResults.PeakType.Stokes,)
705                #AntiStokes
706                if qt in antiStokes[0]:
707                    res[qt.name][Data.AnalysisResults.PeakType.AntiStokes.name] = antiStokes[1][antiStokes[0].index(qt)]
708                    pts += (Data.AnalysisResults.PeakType.AntiStokes,)
709                #average getting the units of the first peak
710                res[qt.name][Data.AnalysisResults.PeakType.average.name] = Metadata.Item(
711                    np.mean([np.abs(res[qt.name][pt.name].value) for pt in pts]), 
712                    res[qt.name][pts[0].name].units
713                    )
714                if not all(res[qt.name][pt.name].units == res[qt.name][pts[0].name].units for pt in pts):
715                    warnings.warn(f"The units of {pts} are not consistent.")
716            return res
717
718        @classmethod
719        def _get_quantity_name(cls, qt: Quantity, pt: PeakType, index: int) -> str:
720            """
721            Returns the name of the dataset correponding to the specific Quantity, PeakType and index
722
723            Args:
724                qt (Quantity)   
725                pt (PeakType)  
726                intex (int): in case of multiple peaks fitted, the index of the peak to consider       
727            """
728            if not pt in (cls.PeakType.AntiStokes, cls.PeakType.Stokes):
729                raise ValueError("pt has to be either Stokes or AntiStokes")
730            if qt == cls.Quantity.R2 or qt == cls.Quantity.RMSE or qt == cls.Quantity.Cov_matrix:
731                name = f"Fit_error_{str(pt.value)}_{index}/{str(qt.value)}"
732            else:
733                name = f"{str(qt.value)}_{str(pt.value)}_{index}"
734            return name
735
736        async def _get_quantity(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0):
737            """
738            Retrieve a specific quantity dataset from the file.
739
740            Args:
741                qt (Quantity): The type of quantity to retrieve.
742                pt (PeakType, optional): The peak type to consider (default is PeakType.AntiStokes).
743                index (int, optional): The index of the quantity if multiple peaks are available (default is 0).
744
745            Returns:
746                The dataset corresponding to the specified quantity, as stored in the file.
747
748            """
749
750            dt_name = Data.AnalysisResults._get_quantity_name(qt, pt, index)
751            full_path = concatenate_paths(self._path, dt_name)
752            return await self._file.open_dataset(full_path)
753
754        def list_existing_peak_types(self, index: int = 0) -> tuple:
755            """
756            Synchronous wrapper for `list_existing_peak_types_async` (see doc for `brimfile.data.Data.AnalysisResults.list_existing_peak_types_async`)
757            """
758            return sync(self.list_existing_peak_types_async(index)) 
759        async def list_existing_peak_types_async(self, index: int = 0) -> tuple:
760            """
761            Returns a tuple of existing peak types (Stokes and/or AntiStokes) for the specified index.
762            Args:
763                index (int, optional): The index of the peak to check (in case of multi-peak fit). Defaults to 0.
764            Returns:
765                tuple: A tuple containing `PeakType` members (`Stokes`, `AntiStokes`) that exist for the given index.
766            """
767
768            as_cls = Data.AnalysisResults
769            shift_s_name = as_cls._get_quantity_name(
770                as_cls.Quantity.Shift, as_cls.PeakType.Stokes, index)
771            shift_as_name = as_cls._get_quantity_name(
772                as_cls.Quantity.Shift, as_cls.PeakType.AntiStokes, index)
773            ls = ()
774            coro_as_exists = self._file.object_exists(concatenate_paths(self._path, shift_as_name))
775            coro_s_exists = self._file.object_exists(concatenate_paths(self._path, shift_s_name))
776            as_exists, s_exists = await asyncio.gather(coro_as_exists, coro_s_exists)
777            if as_exists:
778                ls += (as_cls.PeakType.AntiStokes,)
779            if s_exists:
780                ls += (as_cls.PeakType.Stokes,)
781            return ls
782
783        def list_existing_quantities(self,  pt: PeakType = PeakType.AntiStokes, index: int = 0) -> tuple:
784            """
785            Synchronous wrapper for `list_existing_quantities_async` (see doc for `brimfile.data.Data.AnalysisResults.list_existing_quantities_async`)
786            """
787            return sync(self.list_existing_quantities_async(pt, index))
788        async def list_existing_quantities_async(self,  pt: PeakType = PeakType.AntiStokes, index: int = 0) -> tuple:
789            """
790            Returns a tuple of existing quantities for the specified index.
791            Args:
792                index (int, optional): The index of the peak to check (in case of multi-peak fit). Defaults to 0.
793            Returns:
794                tuple: A tuple containing `Quantity` members that exist for the given index.
795            """
796            as_cls = Data.AnalysisResults
797            ls = ()
798
799            qts = [qt for qt in as_cls.Quantity]
800            coros = [self._file.object_exists(concatenate_paths(self._path, as_cls._get_quantity_name(qt, pt, index))) for qt in qts]
801            
802            qt_exists = await asyncio.gather(*coros)
803            for i, exists in enumerate(qt_exists):
804                if exists:
805                    ls += (qts[i],)
806            return ls

Rapresents the analysis results associated with a Data object.

Data.AnalysisResults( file: brimfile.file_abstraction.FileAbstraction, full_path: str, spatial_map, spatial_map_px_size)
350        def __init__(self, file: FileAbstraction, full_path: str, spatial_map, spatial_map_px_size):
351            """
352            Initialize the AnalysisResults object.
353
354            Args:
355                file (File): The parent File object.
356                full_path (str): path of the group storing the analysis results
357            """
358            self._file = file
359            self._path = full_path
360            # self._group = file.open_group(full_path)
361            self._spatial_map = spatial_map
362            self._spatial_map_px_size = spatial_map_px_size

Initialize the AnalysisResults object.

Arguments:
  • file (File): The parent File object.
  • full_path (str): path of the group storing the analysis results
def get_name(self):
364        def get_name(self):
365            """
366            Returns the name of the Analysis group.
367            """
368            return sync(get_object_name(self._file, self._path))

Returns the name of the Analysis group.

def add_data( self, data_AntiStokes=None, data_Stokes=None, fit_model: Data.AnalysisResults.FitModel = None):
387        def add_data(self, data_AntiStokes=None, data_Stokes=None, fit_model: 'Data.AnalysisResults.FitModel' = None):
388            """
389            Adds data for the analysis results for AntiStokes and Stokes peaks to the file.
390            
391            Args:
392                data_AntiStokes (dict or list[dict]): A dictionary containing the analysis results for AntiStokes peaks.
393                    In case multiple peaks were fitted, it might be a list of dictionaries with each element corresponding to a single peak.
394                
395                    Each dictionary may include the following keys (plus the corresponding units,  e.g. 'shift_units'):
396                        - 'shift': The shift value.
397                        - 'width': The width value.
398                        - 'amplitude': The amplitude value.
399                        - 'offset': The offset value.
400                        - 'R2': The R-squared value.
401                        - 'RMSE': The root mean square error value.
402                        - 'Cov_matrix': The covariance matrix.
403                data_Stokes (dict or list[dict]): same as `data_AntiStokes` for the Stokes peaks.
404                fit_model (Data.AnalysisResults.FitModel, optional): The fit model used for the analysis. Defaults to None (no attribute is set).
405
406                Both `data_AntiStokes` and `data_Stokes` are optional, but at least one of them must be provided.
407            """
408
409            ar_cls = Data.AnalysisResults
410            ar_group = sync(self._file.open_group(self._path))
411
412            def add_quantity(qt: Data.AnalysisResults.Quantity, pt: Data.AnalysisResults.PeakType, data, index: int = 0):
413                # TODO: check if the data is valid
414                sync(self._file.create_dataset(
415                    ar_group, ar_cls._get_quantity_name(qt, pt, index), data))
416
417            def add_data_pt(pt: Data.AnalysisResults.PeakType, data, index: int = 0):
418                if 'shift' in data:
419                    add_quantity(ar_cls.Quantity.Shift,
420                                 pt, data['shift'], index)
421                    if 'shift_units' in data:
422                        self._set_units(data['shift_units'],
423                                        ar_cls.Quantity.Shift, pt, index)
424                if 'width' in data:
425                    add_quantity(ar_cls.Quantity.Width,
426                                 pt, data['width'], index)
427                    if 'width_units' in data:
428                        self._set_units(data['width_units'],
429                                        ar_cls.Quantity.Width, pt, index)
430                if 'amplitude' in data:
431                    add_quantity(ar_cls.Quantity.Amplitude,
432                                 pt, data['amplitude'], index)
433                    if 'amplitude_units' in data:
434                        self._set_units(
435                            data['amplitude_units'], ar_cls.Quantity.Amplitude, pt, index)
436                if 'offset' in data:
437                    add_quantity(ar_cls.Quantity.Offset,
438                                 pt, data['offset'], index)
439                    if 'offset_units' in data:
440                        self._set_units(
441                            data['offset_units'], ar_cls.Quantity.Offset, pt, index)
442                if 'R2' in data:
443                    add_quantity(ar_cls.Quantity.R2, pt, data['R2'], index)
444                    if 'R2_units' in data:
445                        self._set_units(data['R2_units'],
446                                        ar_cls.Quantity.R2, pt, index)
447                if 'RMSE' in data:
448                    add_quantity(ar_cls.Quantity.RMSE, pt, data['RMSE'], index)
449                    if 'RMSE_units' in data:
450                        self._set_units(data['RMSE_units'],
451                                        ar_cls.Quantity.RMSE, pt, index)
452                if 'Cov_matrix' in data:
453                    add_quantity(ar_cls.Quantity.Cov_matrix,
454                                 pt, data['Cov_matrix'], index)
455                    if 'Cov_matrix_units' in data:
456                        self._set_units(
457                            data['Cov_matrix_units'], ar_cls.Quantity.Cov_matrix, pt, index)
458
459            if data_AntiStokes is not None:
460                data_AntiStokes = var_to_singleton(data_AntiStokes)
461                for i, d_as in enumerate(data_AntiStokes):
462                    add_data_pt(ar_cls.PeakType.AntiStokes, d_as, i)
463            if data_Stokes is not None:
464                data_Stokes = var_to_singleton(data_Stokes)
465                for i, d_s in enumerate(data_Stokes):
466                    add_data_pt(ar_cls.PeakType.Stokes, d_s, i)
467            if fit_model is not None:
468                sync(self._file.create_attr(ar_group, 'Fit_model', fit_model.value))

Adds data for the analysis results for AntiStokes and Stokes peaks to the file.

Arguments:
  • data_AntiStokes (dict or list[dict]): A dictionary containing the analysis results for AntiStokes peaks. In case multiple peaks were fitted, it might be a list of dictionaries with each element corresponding to a single peak.

    Each dictionary may include the following keys (plus the corresponding units, e.g. 'shift_units'): - 'shift': The shift value. - 'width': The width value. - 'amplitude': The amplitude value. - 'offset': The offset value. - 'R2': The R-squared value. - 'RMSE': The root mean square error value. - 'Cov_matrix': The covariance matrix.

  • data_Stokes (dict or list[dict]): same as data_AntiStokes for the Stokes peaks.
  • fit_model (Data.AnalysisResults.FitModel, optional): The fit model used for the analysis. Defaults to None (no attribute is set).
  • Both data_AntiStokes and data_Stokes are optional, but at least one of them must be provided.
def get_units( self, qt: Data.AnalysisResults.Quantity, pt: Data.AnalysisResults.PeakType = <PeakType.AntiStokes: 'AS'>, index: int = 0) -> str:
470        def get_units(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0) -> str:
471            """
472            Retrieve the units of a specified quantity from the data file.
473
474            Args:
475                qt (Quantity): The quantity for which the units are to be retrieved.
476                pt (PeakType, optional): The type of peak (e.g., Stokes or AntiStokes). Defaults to PeakType.AntiStokes.
477                index (int, optional): The index of the quantity in case multiple quantities exist. Defaults to 0.
478
479            Returns:
480                str: The units of the specified quantity as a string.
481            """
482            dt_name = Data.AnalysisResults._get_quantity_name(qt, pt, index)
483            full_path = concatenate_paths(self._path, dt_name)
484            return sync(units.of_object(self._file, full_path))

Retrieve the units of a specified quantity from the data file.

Arguments:
  • qt (Quantity): The quantity for which the units are to be retrieved.
  • pt (PeakType, optional): The type of peak (e.g., Stokes or AntiStokes). Defaults to PeakType.AntiStokes.
  • index (int, optional): The index of the quantity in case multiple quantities exist. Defaults to 0.
Returns:

str: The units of the specified quantity as a string.

fit_model: Data.AnalysisResults.FitModel
503        @property
504        def fit_model(self) -> 'Data.AnalysisResults.FitModel':
505            """
506            Retrieve the fit model used for the analysis.
507
508            Returns:
509                Data.AnalysisResults.FitModel: The fit model used for the analysis.
510            """
511            if not hasattr(self, '_fit_model'):
512                try:
513                    fit_model_str = sync(self._file.get_attr(self._path, 'Fit_model'))
514                    self._fit_model = Data.AnalysisResults.FitModel(fit_model_str)
515                except Exception as e:
516                    if isinstance(e, ValueError):
517                        warnings.warn(
518                            f"Unknown fit model '{fit_model_str}' found in the file.")
519                    self._fit_model = Data.AnalysisResults.FitModel.Undefined        
520            return self._fit_model

Retrieve the fit model used for the analysis.

Returns:

Data.AnalysisResults.FitModel: The fit model used for the analysis.

def save_image_to_OMETiff( self, qt: Data.AnalysisResults.Quantity, pt: Data.AnalysisResults.PeakType = <PeakType.AntiStokes: 'AS'>, index: int = 0, filename: str = None) -> str:
522        def save_image_to_OMETiff(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0, filename: str = None) -> str:
523            """
524            Saves the image corresponding to the specified quantity and index to an OMETiff file.
525
526            Args:
527                qt (Quantity): The quantity to retrieve the image for (e.g. shift).
528                pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes).
529                index (int, optional): The index of the data to retrieve, if multiple are present (default is 0).
530                filename (str, optional): The name of the file to save the image to. If None, a default name will be used.
531
532            Returns:
533                str: The path to the saved OMETiff file.
534            """
535            try:
536                import tifffile
537            except ImportError:
538                raise ModuleNotFoundError(
539                    "The tifffile module is required for saving to OME-Tiff. Please install it using 'pip install tifffile'.")
540            
541            if filename is None:
542                filename = f"{qt.value}_{pt.value}_{index}.ome.tif"
543            if not filename.endswith('.ome.tif'):
544                filename += '.ome.tif'
545            img, px_size = self.get_image(qt, pt, index)
546            if img.ndim > 3:
547                raise NotImplementedError(
548                    "Saving images with more than 3 dimensions is not supported yet.")
549            with tifffile.TiffWriter(filename, bigtiff=True) as tif:
550                metadata = {
551                    'axes': 'ZYX',
552                    'PhysicalSizeX': px_size[2].value,
553                    'PhysicalSizeXUnit': px_size[2].units,
554                    'PhysicalSizeY': px_size[1].value,
555                    'PhysicalSizeYUnit': px_size[1].units,
556                    'PhysicalSizeZ': px_size[0].value,
557                    'PhysicalSizeZUnit': px_size[0].units,
558                }
559                tif.write(img, metadata=metadata)
560            return filename

Saves the image corresponding to the specified quantity and index to an OMETiff file.

Arguments:
  • qt (Quantity): The quantity to retrieve the image for (e.g. shift).
  • pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes).
  • index (int, optional): The index of the data to retrieve, if multiple are present (default is 0).
  • filename (str, optional): The name of the file to save the image to. If None, a default name will be used.
Returns:

str: The path to the saved OMETiff file.

def get_image( self, qt: Data.AnalysisResults.Quantity, pt: Data.AnalysisResults.PeakType = <PeakType.AntiStokes: 'AS'>, index: int = 0) -> tuple:
562        def get_image(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0) -> tuple:
563            """
564            Retrieves an image (spatial map) based on the specified quantity, peak type, and index.
565
566            Args:
567                qt (Quantity): The quantity to retrieve the image for (e.g. shift).
568                pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes).
569                index (int, optional): The index of the data to retrieve, if multiple are present (default is 0).
570
571            Returns:
572                A tuple containing the image corresponding to the specified quantity and index and the corresponding pixel size.
573                The image is a 3D dataset where the dimensions are z, y, x.
574                If there are additional parameters, more dimensions are added in the order z, y, x, par1, par2, ...
575                The pixel size is a tuple of 3 Metadata.Item in the order z, y, x.
576            """
577            pt_type = Data.AnalysisResults.PeakType
578            data = None
579            if pt == pt_type.average:
580                peaks = self.list_existing_peak_types(index)
581                match len(peaks):
582                    case 0:
583                        raise ValueError(
584                            "No peaks found for the specified index. Cannot compute average.")
585                    case 1:
586                        data = np.array(sync(self._get_quantity(qt, peaks[0], index)))
587                    case 2:
588                        data1, data2 = _gather_sync(
589                            self._get_quantity(qt, peaks[0], index),
590                            self._get_quantity(qt, peaks[1], index)
591                            )
592                        data = (np.abs(data1) + np.abs(data2))/2
593            else:
594                data = np.array(sync(self._get_quantity(qt, pt, index)))
595            sm = np.array(self._spatial_map)
596            img = data[sm, ...]
597            img[sm<0, ...] = np.nan  # set invalid pixels to NaN
598            return img, self._spatial_map_px_size

Retrieves an image (spatial map) based on the specified quantity, peak type, and index.

Arguments:
  • qt (Quantity): The quantity to retrieve the image for (e.g. shift).
  • pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes).
  • index (int, optional): The index of the data to retrieve, if multiple are present (default is 0).
Returns:

A tuple containing the image corresponding to the specified quantity and index and the corresponding pixel size. The image is a 3D dataset where the dimensions are z, y, x. If there are additional parameters, more dimensions are added in the order z, y, x, par1, par2, ... The pixel size is a tuple of 3 Metadata.Item in the order z, y, x.

def get_quantity_at_pixel( self, coord: tuple, qt: Data.AnalysisResults.Quantity, pt: Data.AnalysisResults.PeakType = <PeakType.AntiStokes: 'AS'>, index: int = 0):
599        def get_quantity_at_pixel(self, coord: tuple, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0):
600            """
601            Synchronous wrapper for `get_quantity_at_pixel_async` (see doc for `brimfile.data.Data.AnalysisResults.get_quantity_at_pixel_async`)
602            """
603            return sync(self.get_quantity_at_pixel_async(coord, qt, pt, index))
async def get_quantity_at_pixel_async( self, coord: tuple, qt: Data.AnalysisResults.Quantity, pt: Data.AnalysisResults.PeakType = <PeakType.AntiStokes: 'AS'>, index: int = 0):
604        async def get_quantity_at_pixel_async(self, coord: tuple, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0):
605            """
606            Retrieves the specified quantity in the image at coord, based on the peak type and index.
607
608            Args:
609                coord (tuple): A tuple of 3 elements corresponding to the z, y, x coordinate in the image
610                qt (Quantity): The quantity to retrieve the image for (e.g. shift).
611                pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes).
612                index (int, optional): The index of the data to retrieve, if multiple peaks are present (default is 0).
613
614            Returns:
615                The requested quantity, which is a scalar or a multidimensional array (depending on whether there are additional parameters in the current Data group)
616            """
617            if len(coord) != 3:
618                raise ValueError(
619                    "'coord' must have 3 elements corresponding to z, y, x")
620            i = self._spatial_map[*coord]
621            assert i.size == 1
622            if i<0:
623                return np.nan  # invalid pixel
624            i = int(i)
625
626            pt_type = Data.AnalysisResults.PeakType
627            value = None
628            if pt == pt_type.average:
629                value = None
630                peaks = await self.list_existing_peak_types_async(index)
631                match len(peaks):
632                    case 0:
633                        raise ValueError(
634                            "No peaks found for the specified index. Cannot compute average.")
635                    case 1:
636                        data = await self._get_quantity(qt, peaks[0], index)
637                        value = await _async_getitem(data, (i, ...))
638                    case 2:
639                        data_p0, data_p1 = await asyncio.gather(
640                            self._get_quantity(qt, peaks[0], index),
641                            self._get_quantity(qt, peaks[1], index)
642                        )
643                        value1, value2 = await asyncio.gather(
644                            _async_getitem(data_p0, (i, ...)),
645                            _async_getitem(data_p1, (i, ...))
646                        )
647                        value = (np.abs(value1) + np.abs(value2))/2
648            else:
649                data = await self._get_quantity(qt, pt, index)
650                value = await _async_getitem(data, (i, ...))
651            return value

Retrieves the specified quantity in the image at coord, based on the peak type and index.

Arguments:
  • coord (tuple): A tuple of 3 elements corresponding to the z, y, x coordinate in the image
  • qt (Quantity): The quantity to retrieve the image for (e.g. shift).
  • pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes).
  • index (int, optional): The index of the data to retrieve, if multiple peaks are present (default is 0).
Returns:

The requested quantity, which is a scalar or a multidimensional array (depending on whether there are additional parameters in the current Data group)

def get_all_quantities_in_image(self, coor: tuple, index_peak: int = 0) -> dict:
652        def get_all_quantities_in_image(self, coor: tuple, index_peak: int = 0) -> dict:
653            """
654            Retrieve all available quantities at a specific spatial coordinate.
655            see `brimfile.data.Data.AnalysisResults._get_all_quantities_at_index` for more details
656            TODO complete the documentation
657            """
658            if len(coor) != 3:
659                raise ValueError("coor must contain 3 values for z, y, x")
660            index = int(self._spatial_map[coor])
661            return sync(self._get_all_quantities_at_index(index, index_peak))

Retrieve all available quantities at a specific spatial coordinate. see brimfile.data.Data.AnalysisResults._get_all_quantities_at_index for more details TODO complete the documentation

def list_existing_peak_types(self, index: int = 0) -> tuple:
754        def list_existing_peak_types(self, index: int = 0) -> tuple:
755            """
756            Synchronous wrapper for `list_existing_peak_types_async` (see doc for `brimfile.data.Data.AnalysisResults.list_existing_peak_types_async`)
757            """
758            return sync(self.list_existing_peak_types_async(index)) 
async def list_existing_peak_types_async(self, index: int = 0) -> tuple:
759        async def list_existing_peak_types_async(self, index: int = 0) -> tuple:
760            """
761            Returns a tuple of existing peak types (Stokes and/or AntiStokes) for the specified index.
762            Args:
763                index (int, optional): The index of the peak to check (in case of multi-peak fit). Defaults to 0.
764            Returns:
765                tuple: A tuple containing `PeakType` members (`Stokes`, `AntiStokes`) that exist for the given index.
766            """
767
768            as_cls = Data.AnalysisResults
769            shift_s_name = as_cls._get_quantity_name(
770                as_cls.Quantity.Shift, as_cls.PeakType.Stokes, index)
771            shift_as_name = as_cls._get_quantity_name(
772                as_cls.Quantity.Shift, as_cls.PeakType.AntiStokes, index)
773            ls = ()
774            coro_as_exists = self._file.object_exists(concatenate_paths(self._path, shift_as_name))
775            coro_s_exists = self._file.object_exists(concatenate_paths(self._path, shift_s_name))
776            as_exists, s_exists = await asyncio.gather(coro_as_exists, coro_s_exists)
777            if as_exists:
778                ls += (as_cls.PeakType.AntiStokes,)
779            if s_exists:
780                ls += (as_cls.PeakType.Stokes,)
781            return ls

Returns a tuple of existing peak types (Stokes and/or AntiStokes) for the specified index.

Arguments:
  • index (int, optional): The index of the peak to check (in case of multi-peak fit). Defaults to 0.
Returns:

tuple: A tuple containing PeakType members (Stokes, AntiStokes) that exist for the given index.

def list_existing_quantities( self, pt: Data.AnalysisResults.PeakType = <PeakType.AntiStokes: 'AS'>, index: int = 0) -> tuple:
783        def list_existing_quantities(self,  pt: PeakType = PeakType.AntiStokes, index: int = 0) -> tuple:
784            """
785            Synchronous wrapper for `list_existing_quantities_async` (see doc for `brimfile.data.Data.AnalysisResults.list_existing_quantities_async`)
786            """
787            return sync(self.list_existing_quantities_async(pt, index))
async def list_existing_quantities_async( self, pt: Data.AnalysisResults.PeakType = <PeakType.AntiStokes: 'AS'>, index: int = 0) -> tuple:
788        async def list_existing_quantities_async(self,  pt: PeakType = PeakType.AntiStokes, index: int = 0) -> tuple:
789            """
790            Returns a tuple of existing quantities for the specified index.
791            Args:
792                index (int, optional): The index of the peak to check (in case of multi-peak fit). Defaults to 0.
793            Returns:
794                tuple: A tuple containing `Quantity` members that exist for the given index.
795            """
796            as_cls = Data.AnalysisResults
797            ls = ()
798
799            qts = [qt for qt in as_cls.Quantity]
800            coros = [self._file.object_exists(concatenate_paths(self._path, as_cls._get_quantity_name(qt, pt, index))) for qt in qts]
801            
802            qt_exists = await asyncio.gather(*coros)
803            for i, exists in enumerate(qt_exists):
804                if exists:
805                    ls += (qts[i],)
806            return ls

Returns a tuple of existing quantities for the specified index.

Arguments:
  • index (int, optional): The index of the peak to check (in case of multi-peak fit). Defaults to 0.
Returns:

tuple: A tuple containing Quantity members that exist for the given index.

class Data.AnalysisResults.Quantity(enum.Enum):
325        class Quantity(Enum):
326            """
327            Enum representing the type of analysis results.
328            """
329            Shift = "Shift"
330            Width = "Width"
331            Amplitude = "Amplitude"
332            Offset = "Offset"
333            R2 = "R2"
334            RMSE = "RMSE"
335            Cov_matrix = "Cov_matrix"

Enum representing the type of analysis results.

Shift = <Quantity.Shift: 'Shift'>
Width = <Quantity.Width: 'Width'>
Amplitude = <Quantity.Amplitude: 'Amplitude'>
Offset = <Quantity.Offset: 'Offset'>
R2 = <Quantity.R2: 'R2'>
RMSE = <Quantity.RMSE: 'RMSE'>
Cov_matrix = <Quantity.Cov_matrix: 'Cov_matrix'>
class Data.AnalysisResults.PeakType(enum.Enum):
337        class PeakType(Enum):
338            AntiStokes = "AS"
339            Stokes = "S"
340            average = "avg"
AntiStokes = <PeakType.AntiStokes: 'AS'>
Stokes = <PeakType.Stokes: 'S'>
average = <PeakType.average: 'avg'>
class Data.AnalysisResults.FitModel(enum.Enum):
342        class FitModel(Enum):
343            Undefined = "Undefined"
344            Lorentzian = "Lorentzian"
345            DHO = "DHO"
346            Gaussian = "Gaussian"
347            Voigt = "Voigt"
348            Custom = "Custom"
Undefined = <FitModel.Undefined: 'Undefined'>
Lorentzian = <FitModel.Lorentzian: 'Lorentzian'>
DHO = <FitModel.DHO: 'DHO'>
Gaussian = <FitModel.Gaussian: 'Gaussian'>
Voigt = <FitModel.Voigt: 'Voigt'>
Custom = <FitModel.Custom: 'Custom'>