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}"
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.
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.
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.
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.
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.
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.
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)
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.
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.
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
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
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.
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.
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
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
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.
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
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
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.
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.
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
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.
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_AntiStokesfor 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_AntiStokesanddata_Stokesare optional, but at least one of them must be provided.
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.
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.
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.
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.
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))
Synchronous wrapper for get_quantity_at_pixel_async (see doc for brimfile.data.Data.AnalysisResults.get_quantity_at_pixel_async)
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)
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
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))
Synchronous wrapper for list_existing_peak_types_async (see doc for brimfile.data.Data.AnalysisResults.list_existing_peak_types_async)
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
PeakTypemembers (Stokes,AntiStokes) that exist for the given index.
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))
Synchronous wrapper for list_existing_quantities_async (see doc for brimfile.data.Data.AnalysisResults.list_existing_quantities_async)
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
Quantitymembers that exist for the given index.
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.