brimfile.data
1import numpy as np 2import asyncio 3 4import warnings 5 6from .file_abstraction import FileAbstraction, sync, _async_getitem, _gather_sync 7from .utils import concatenate_paths, list_objects_matching_pattern, get_object_name, set_object_name 8from .utils import np_array_to_smallest_int_type, _guess_chunks 9 10from .metadata import Metadata 11 12from numbers import Number 13 14from . import units 15from .analysis_results import AnalysisResults 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 # make AnalysisResults available as an attribute of Data 26 AnalysisResults = AnalysisResults 27 28 def __init__(self, file: FileAbstraction, path: str, *, newly_created = False): 29 """ 30 Initialize the Data object. This constructor should not be called directly. 31 32 Args: 33 file (File): The parent File object. 34 path (str): The path to the data group within the file. 35 newly_created (bool): Whether this data group is being created as new. 36 If True, the constructor will not attempt to load spatial mapping. 37 """ 38 self._file = file 39 self._path = path 40 self._group = sync(file.open_group(path)) 41 42 self._sparse = self._load_sparse_flag() 43 # the _spatial_map is None for non sparse data but the _spatial_map_px_size should always be valid 44 self._spatial_map, self._spatial_map_px_size = self._load_spatial_mapping() if not newly_created else (None, None) 45 46 def get_name(self): 47 """ 48 Returns the name of the data group. 49 """ 50 return sync(get_object_name(self._file, self._path)) 51 52 def get_index(self): 53 """ 54 Returns the index of the data group. 55 """ 56 return int(self._path.split('/')[-1].split('_')[-1]) 57 58 def _load_sparse_flag(self) -> bool: 59 """ 60 Load the 'Sparse' flag for the data group. 61 62 Returns: 63 bool: The value of the 'Sparse' flag, or False if the attribute is not found or invalid. 64 """ 65 try: 66 sparse = sync(self._file.get_attr(self._group, 'Sparse')) 67 if isinstance(sparse, bool): 68 return sparse 69 else: 70 warnings.warn( 71 f"Invalid value for 'Sparse' attribute in {self._path}. Expected a boolean, got {type(sparse)}. Defaulting to False.") 72 return False 73 except Exception: 74 # if the attribute is not found, return the default value False 75 return False 76 77 def _load_spatial_mapping(self, load_in_memory: bool=True) -> tuple: 78 """ 79 Load a spatial mapping in the same format as 'Cartesian visualisation', 80 irrespectively on whether 'Spatial_map' is defined instead. 81 -1 is used for "empty" pixels in the image 82 Args: 83 load_in_memory (bool): Specify whether the map should be forced to load in memory or just opened as a dataset. 84 Returns: 85 The spatial map and the corresponding pixel size as a tuple of 3 Metadata.Item, both in the order z, y, x. 86 If the spatial mapping is not defined in the file, returns None for the spatial map. 87 The pixel size is read from the data group for non-sparse data. 88 """ 89 cv = None 90 px_size = 3*(Metadata.Item(value=1, units=None),) 91 92 cv_path = concatenate_paths( 93 self._path, brim_obj_names.data.cartesian_visualisation) 94 sm_path = concatenate_paths( 95 self._path, brim_obj_names.data.spatial_map) 96 97 if sync(self._file.object_exists(cv_path)): 98 cv = sync(self._file.open_dataset(cv_path)) 99 100 #read the pixel size from the 'Cartesian visualisation' dataset 101 px_size_val = None 102 px_size_units = None 103 try: 104 px_size_val = sync(self._file.get_attr(cv, 'element_size')) 105 if px_size_val is None or len(px_size_val) != 3: 106 raise ValueError( 107 "The 'element_size' attribute of 'Cartesian_visualisation' must be a tuple of 3 elements") 108 except Exception: 109 px_size_val = 3*(1,) 110 warnings.warn( 111 "No pixel size defined for Cartesian visualisation") 112 px_size_units = sync(units.of_attribute( 113 self._file, cv, 'element_size')) 114 px_size = () 115 for i in range(3): 116 # if px_size_val[i] is not a number, set it to 1 and px_size_units to None 117 if isinstance(px_size_val[i], Number): 118 px_size += (Metadata.Item(px_size_val[i], px_size_units), ) 119 else: 120 px_size += (Metadata.Item(1, None), ) 121 122 123 if load_in_memory: 124 cv = np.array(cv) 125 cv = np_array_to_smallest_int_type(cv) 126 127 elif sync(self._file.object_exists(sm_path)): 128 def load_spatial_map_from_file(self): 129 async def load_coordinate_from_sm(coord: str): 130 res = np.empty(0) # empty array 131 try: 132 res = await self._file.open_dataset( 133 concatenate_paths(sm_path, coord)) 134 res = await res.to_np_array() 135 res = np.squeeze(res) # remove single-dimensional entries 136 except Exception as e: 137 # if the coordinate does not exist, return an empty array 138 pass 139 if len(res.shape) > 1: 140 raise ValueError( 141 f"The 'Spatial_map/{coord}' dataset is not a 1D array as expected") 142 return res 143 144 def check_coord_array(arr, size): 145 if arr.size == 0: 146 return np.zeros(size) 147 elif arr.size != size: 148 raise ValueError( 149 "The 'Spatial_map' dataset is invalid") 150 return arr 151 152 x, y, z = _gather_sync( 153 load_coordinate_from_sm('x'), 154 load_coordinate_from_sm('y'), 155 load_coordinate_from_sm('z') 156 ) 157 size = max([x.size, y.size, z.size]) 158 if size == 0: 159 raise ValueError("The 'Spatial_map' dataset is empty") 160 x = check_coord_array(x, size) 161 y = check_coord_array(y, size) 162 z = check_coord_array(z, size) 163 return x, y, z 164 165 def calculate_step(x): 166 n = len(np.unique(x)) 167 if n == 1: 168 d = None 169 else: 170 d = (np.max(x)-np.min(x))/(n-1) 171 return n, d 172 173 x, y, z = load_spatial_map_from_file(self) 174 175 # TODO extend the reconstruction to non-cartesian cases 176 177 nX, dX = calculate_step(x) 178 nY, dY = calculate_step(y) 179 nZ, dZ = calculate_step(z) 180 181 indices = np_array_to_smallest_int_type(np.lexsort((x, y, z))) 182 cv = np.reshape(indices, (nZ, nY, nX)) 183 184 px_size_units = sync(units.of_object(self._file, sm_path)) 185 px_size = () 186 for i in range(3): 187 px_sz = (dZ, dY, dX)[i] 188 px_unit = px_size_units 189 if px_sz is None: 190 px_sz = 1 191 px_unit = None 192 px_size += (Metadata.Item(px_sz, px_unit),) 193 elif not self._sparse: 194 try: 195 px_sz = sync(self._file.get_attr(self._group, 'element_size')) 196 if len(px_sz) != 3: 197 raise ValueError( 198 "The 'element_size' attribute must be a tuple of 3 elements") 199 px_unit = None 200 try: 201 px_unit = sync(units.of_attribute(self._file, self._group, 'element_size')) 202 except Exception: 203 warnings.warn("Pixel size unit is not provided for non-sparse data.") 204 px_size = tuple(Metadata.Item(el, px_unit) for el in px_sz) 205 except Exception: 206 warnings.warn("Pixel size is not provided for non-sparse data.") 207 208 return cv, px_size 209 210 def get_PSD(self) -> tuple: 211 """ 212 LOW LEVEL FUNCTION 213 214 Retrieve the Power Spectral Density (PSD) and frequency from the current data group. 215 Note: this function exposes the internals of the brim file and thus the interface might change in future versions. 216 Use only if more specialized functions are not working for your application! 217 Returns: 218 tuple: (PSD, frequency, PSD_units, frequency_units) 219 - 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). 220 - 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). 221 - PSD_units: The units of the PSD. 222 - frequency_units: The units of the frequency. 223 """ 224 warnings.warn( 225 "Data.get_PSD is deprecated and will be removed in a future release. " 226 "Use Data.get_PSD_as_spatial_map instead.", 227 DeprecationWarning, 228 stacklevel=2, 229 ) 230 PSD, frequency = _gather_sync( 231 self._file.open_dataset(concatenate_paths( 232 self._path, brim_obj_names.data.PSD)), 233 self._file.open_dataset(concatenate_paths( 234 self._path, brim_obj_names.data.frequency)) 235 ) 236 # retrieve the units of the PSD and frequency 237 PSD_units, frequency_units = _gather_sync( 238 units.of_object(self._file, PSD), 239 units.of_object(self._file, frequency) 240 ) 241 242 return PSD, frequency, PSD_units, frequency_units 243 244 def get_PSD_as_spatial_map(self) -> tuple: 245 """ 246 Retrieve the Power Spectral Density (PSD) as a spatial map and the frequency from the current data group. 247 Returns: 248 tuple: (PSD, frequency, PSD_units, frequency_units) 249 - PSD: A 4D (or more) numpy array containing all the spectra. Dimensions are z, y, x, [parameters], spectrum. 250 - frequency: A numpy array representing the frequency data, which can be broadcastable to PSD. 251 - PSD_units: The units of the PSD. 252 - frequency_units: The units of the frequency. 253 """ 254 PSD, frequency = _gather_sync( 255 self._file.open_dataset(concatenate_paths( 256 self._path, brim_obj_names.data.PSD)), 257 self._file.open_dataset(concatenate_paths( 258 self._path, brim_obj_names.data.frequency)) 259 ) 260 # retrieve the units of the PSD and frequency 261 PSD_units, frequency_units = _gather_sync( 262 units.of_object(self._file, PSD), 263 units.of_object(self._file, frequency) 264 ) 265 266 # ensure PSD and frequency are numpy arrays 267 PSD = np.array(PSD) 268 frequency = np.array(frequency) # ensure it's a numpy array 269 270 #if the frequency is not the same for all spectra, broadcast it to match the shape of PSD 271 if frequency.ndim > 1: 272 frequency = np.broadcast_to(frequency, PSD.shape) 273 274 if self._sparse: 275 if self._spatial_map is None: 276 raise ValueError("The data is defined as sparse, but no spatial mapping is provided.") 277 sm = np.array(self._spatial_map) 278 # reshape the PSD to have the spatial dimensions first 279 PSD = PSD[sm, ...] 280 # reshape the frequency only if it is not the same for all spectra 281 if frequency.ndim > 1: 282 frequency = frequency[sm, ...] 283 284 return PSD, frequency, PSD_units, frequency_units 285 286 def _get_spectrum(self, index: int | tuple[int, int, int]) -> tuple: 287 """ 288 Synchronous wrapper for `_get_spectrum_async` (see doc for `brimfile.data.Data._get_spectrum_async`) 289 """ 290 return sync(self._get_spectrum_async(index)) 291 async def _get_spectrum_async(self, index: int | tuple[int, int, int]) -> tuple: 292 """ 293 Retrieve a spectrum from the data group by its index or coordinates. 294 295 Args: 296 index (int | tuple[int, int, int]): The index (for sparse data) or z, y, x coordinates (for non-sparse data) of the spectrum to retrieve. 297 298 Returns: 299 tuple: (PSD, frequency, PSD_units, frequency_units) for the specified index. 300 PSD can be 1D or more (if there are additional parameters); 301 frequency has the same size as PSD 302 Raises: 303 IndexError: If the index is out of range for the PSD dataset. 304 """ 305 if self._sparse and not isinstance(index, int): 306 raise ValueError("For sparse data, index must be an integer.") 307 elif not self._sparse and not (isinstance(index, tuple) and len(index) == 3): 308 raise ValueError("For non-sparse data, index must be a tuple of (z, y, x) coordinates.") 309 310 # index = -1 corresponds to no spectrum 311 if self._sparse and index < 0: 312 return None, None, None, None 313 elif not self._sparse and any(i < 0 for i in index): 314 return None, None, None, None 315 PSD, frequency = await asyncio.gather( 316 self._file.open_dataset(concatenate_paths( 317 self._path, brim_obj_names.data.PSD)), 318 self._file.open_dataset(concatenate_paths( 319 self._path, brim_obj_names.data.frequency)) 320 ) 321 if self._sparse and index >= PSD.shape[0]: 322 raise IndexError( 323 f"index {index} out of range for PSD with shape {PSD.shape}") 324 elif not self._sparse and any(i >= PSD.shape[j] for j, i in enumerate(index)): 325 raise IndexError( 326 f"index {index} out of range for PSD with shape {PSD.shape}") 327 # retrieve the units of the PSD and frequency 328 PSD_units, frequency_units = await asyncio.gather( 329 units.of_object(self._file, PSD), 330 units.of_object(self._file, frequency) 331 ) 332 # add ellipsis to the index to select the spectrum and the corresponding frequency 333 if self._sparse: 334 index = (index, ...) 335 else: 336 index = index + (..., ) 337 # map index to the frequency array, considering the broadcasting rules 338 index_frequency = index 339 if frequency.ndim < PSD.ndim: 340 if self._sparse: 341 # given the definition of the brim file format, 342 # if the frequency has less dimensions that PSD, 343 # it can only be because it is the same for all the spatial position (first dimension) 344 index_frequency = (..., ) 345 else: 346 unassigned_indices = PSD.ndim - frequency.ndim 347 if unassigned_indices == 3: 348 # if the frequency has no spatial dimension, it is the same for all the spatial positions 349 index_frequency = (..., ) 350 else: 351 # if the frequency has some spatial dimensions but not all, we need to add the corresponding indices to the index of the frequency 352 index_frequency = index[-unassigned_indices:] + (..., ) 353 #get the spectrum and the corresponding frequency at the specified index 354 PSD, frequency = await asyncio.gather( 355 _async_getitem(PSD, index), 356 _async_getitem(frequency, index_frequency) 357 ) 358 #broadcast the frequency to match the shape of PSD if needed 359 if frequency.ndim < PSD.ndim: 360 frequency = np.broadcast_to(frequency, PSD.shape) 361 return PSD, frequency, PSD_units, frequency_units 362 363 def get_spectrum_in_image(self, coor: tuple) -> tuple: 364 """ 365 Retrieve a spectrum from the data group using spatial coordinates. 366 367 Args: 368 coor (tuple): A tuple containing the z, y, x coordinates of the spectrum to retrieve. 369 370 Returns: 371 tuple: A tuple containing the PSD, frequency, PSD_units, frequency_units for the specified coordinates. See `Data._get_spectrum_async` for details. 372 """ 373 if len(coor) != 3: 374 raise ValueError("coor must contain 3 values for z, y, x") 375 376 if self._sparse: 377 index = int(self._spatial_map[coor]) 378 return self._get_spectrum(index) 379 else: 380 return self._get_spectrum(coor) 381 382 def get_spectrum_and_all_quantities_in_image(self, ar: 'Data.AnalysisResults', coor: tuple, index_peak: int = 0): 383 """ 384 Retrieve the spectrum and all available quantities from the analysis results at a specific spatial coordinate. 385 386 Args: 387 ar (Data.AnalysisResults): The analysis results object to retrieve quantities from. 388 coor (tuple): A tuple containing the z, y, x coordinates in the image. 389 index_peak (int, optional): The index of the peak to retrieve (for multi-peak fits). Defaults to 0. 390 391 Returns: 392 tuple: A tuple containing: 393 - spectrum (tuple): (PSD, frequency, PSD_units, frequency_units) at the specified coordinate 394 - quantities (dict): Dictionary of Metadata.Item in the form result[quantity.name][peak.name] 395 """ 396 if len(coor) != 3: 397 raise ValueError("coor must contain 3 values for z, y, x") 398 index = coor 399 if self._sparse: 400 index = int(self._spatial_map[coor]) 401 spectrum, quantities = _gather_sync( 402 self._get_spectrum_async(index), 403 ar._get_all_quantities_at_index(index, index_peak) 404 ) 405 return spectrum, quantities 406 407 def get_metadata(self): 408 """ 409 Returns the metadata associated with the current Data group 410 Note that this contains both the general metadata stored in the file (which might be redifined by the specific data group) 411 and the ones specific for this data group 412 """ 413 return Metadata(self._file, self._path) 414 415 def get_num_parameters(self) -> tuple: 416 """ 417 Retrieves the number of parameters 418 419 Returns: 420 tuple: The shape of the parameters if they exist, otherwise an empty tuple. 421 """ 422 pars, _ = self.get_parameters() 423 return pars.shape if pars is not None else () 424 425 def get_parameters(self) -> list: 426 """ 427 Retrieves the parameters and their associated names. 428 429 If PSD.ndims > 2, the parameters are stored in a separate dataset. 430 431 Returns: 432 list: A tuple containing the parameters and their names if there are any, otherwise None. 433 """ 434 pars_full_path = concatenate_paths( 435 self._path, brim_obj_names.data.parameters) 436 if sync(self._file.object_exists(pars_full_path)): 437 pars = sync(self._file.open_dataset(pars_full_path)) 438 pars_names = sync(self._file.get_attr(pars, 'Name')) 439 return (pars, pars_names) 440 return (None, None) 441 442 def create_analysis_results_group(self, data_AntiStokes, data_Stokes=None, *, 443 index: int = None, name: str = None, fit_model: 'Data.AnalysisResults.FitModel' = None) -> AnalysisResults: 444 """ 445 Adds a new AnalysisResults entry to the current data group. 446 Parameters: 447 data_AntiStokes (dict or list[dict]): see documentation for `brimfile.analysis_results.AnalysisResults.add_data` 448 data_Stokes (dict or list[dict]): same as data_AntiStokes for the Stokes peaks. 449 index (int, optional): The index for the new data entry. If None, the next available index is used. Defaults to None. 450 name (str, optional): The name for the new Analysis group. Defaults to None. 451 fit_model (Data.AnalysisResults.FitModel, optional): The fit model used for the analysis. Defaults to None (no attribute is set). 452 Returns: 453 AnalysisResults: The newly created AnalysisResults object. 454 Raises: 455 IndexError: If the specified index already exists in the dataset. 456 ValueError: If any of the data provided is not valid or consistent 457 """ 458 if index is not None: 459 try: 460 self.get_analysis_results(index) 461 except IndexError: 462 pass 463 else: 464 # If the group already exists, raise an error 465 raise IndexError( 466 f"Analysis {index} already exists in {self._path}") 467 else: 468 ar_groups = self.list_AnalysisResults() 469 indices = [ar['index'] for ar in ar_groups] 470 indices.sort() 471 index = indices[-1] + 1 if indices else 0 # Next available index 472 473 ar = Data.AnalysisResults._create_new(self, index=index, sparse=self._sparse) 474 if name is not None: 475 set_object_name(self._file, ar._path, name) 476 ar.add_data(data_AntiStokes, data_Stokes, fit_model=fit_model) 477 478 return ar 479 480 def list_AnalysisResults(self, retrieve_custom_name=False) -> list: 481 """ 482 List all AnalysisResults groups in the current data group. The list is ordered by index. 483 484 Returns: 485 list: A list of dictionaries, each containing: 486 - 'name' (str): The name of the AnalysisResults group. 487 - 'index' (int): The index extracted from the group name. 488 - 'custom_name' (str, optional): if retrieve_custom_name==True, it contains the name of the AnalysisResults group as returned from utils.get_object_name. 489 """ 490 491 analysis_results_groups = [] 492 493 matched_objs = list_objects_matching_pattern( 494 self._file, self._group, brim_obj_names.data.analysis_results + r"_(\d+)$") 495 async def _make_dict_item(matched_obj, retrieve_custom_name): 496 name = matched_obj[0] 497 index = int(matched_obj[1]) 498 curr_obj_dict = {'name': name, 'index': index} 499 if retrieve_custom_name: 500 ar_path = concatenate_paths(self._path, name) 501 custom_name = await get_object_name(self._file, ar_path) 502 curr_obj_dict['custom_name'] = custom_name 503 return curr_obj_dict 504 coros = [_make_dict_item(matched_obj, retrieve_custom_name) for matched_obj in matched_objs] 505 dicts = _gather_sync(*coros) 506 for dict_item in dicts: 507 analysis_results_groups.append(dict_item) 508 # Sort the data groups by index 509 analysis_results_groups.sort(key=lambda x: x['index']) 510 511 return analysis_results_groups 512 513 def get_analysis_results(self, index: int = 0) -> AnalysisResults: 514 """ 515 Returns the AnalysisResults at the specified index 516 517 Args: 518 index (int) 519 520 Raises: 521 IndexError: If there is no analysis with the corresponding index 522 """ 523 name = None 524 ls = self.list_AnalysisResults() 525 for el in ls: 526 if el['index'] == index: 527 name = el['name'] 528 break 529 if name is None: 530 raise IndexError(f"Analysis {index} not found") 531 path = concatenate_paths(self._path, name) 532 return Data.AnalysisResults(self._file, path, data_group_path=self._path, 533 spatial_map=self._spatial_map, spatial_map_px_size=self._spatial_map_px_size, sparse=self._sparse) 534 535 def _add_data(self, PSD: np.ndarray, frequency: np.ndarray, *, scanning: dict = None, freq_units='GHz', 536 timestamp: np.ndarray = None, compression: FileAbstraction.Compression = FileAbstraction.Compression()): 537 """ 538 Add data to the current data group. 539 540 This method adds the provided PSD, frequency, and scanning data to the HDF5 group 541 associated with this `Data` object. It validates the inputs to ensure they meet 542 the required specifications before adding them. 543 544 Args: 545 PSD (np.ndarray): A 2D numpy array representing the Power Spectral Density (PSD) data. The last dimension contains the spectra. 546 frequency (np.ndarray): A 1D or 2D numpy array representing the frequency data. 547 It must be broadcastable to the shape of the PSD array. 548 scanning (dict, optional): A dictionary containing scanning-related data. 549 Required for sparse data (sparse=True), optional for non-sparse data. 550 For sparse data, must include at least one of 'Spatial_map' or 'Cartesian_visualisation'. 551 It may include the following keys: 552 - 'Spatial_map' (optional): A dictionary containing coordinate arrays: 553 - 'x', 'y', 'z' (optional): 1D numpy arrays of same length with coordinate values 554 - 'units' (optional): string with the unit (e.g., 'um') 555 - 'Cartesian_visualisation' (optional): A 3D numpy array (z, y, x) with integer values 556 mapping spatial positions to spectra indices. Values must be -1 (invalid/empty pixel) 557 or between 0 and PSD.shape[0]-1. 558 - 'Cartesian_visualisation_pixel' (recommended with Cartesian_visualisation): 559 Tuple/list of 3 float values (z, y, x) representing pixel size. Unused dimensions can be None. 560 - 'Cartesian_visualisation_pixel_unit' (optional): String for pixel size unit (default: 'um'). 561 timestamp (np.ndarray, optional): Timestamps in milliseconds for each spectrum. 562 Must be a 1D array with length equal to PSD.shape[0]. 563 564 565 Raises: 566 ValueError: If any of the data provided is not valid or consistent 567 """ 568 569 # Check if frequency is broadcastable to PSD 570 try: 571 np.broadcast_shapes(tuple(frequency.shape), tuple(PSD.shape)) 572 except ValueError as e: 573 raise ValueError(f"frequency (shape: {frequency.shape}) is not broadcastable to PSD (shape: {PSD.shape}): {e}") 574 575 # Check if at least one of 'Spatial_map' or 'Cartesian_visualisation' is present in the scanning dictionary 576 # This is required for sparse data to establish the spatial mapping 577 has_spatial_mapping = False 578 if scanning is not None: 579 if 'Spatial_map' in scanning: 580 sm = scanning['Spatial_map'] 581 size = 0 582 583 def check_coor(coor: str): 584 if coor in sm: 585 sm[coor] = np.array(sm[coor]) 586 size1 = sm[coor].size 587 if size1 != size and size != 0: 588 raise ValueError( 589 f"'{coor}' in 'Spatial_map' is invalid!") 590 return size1 591 size = check_coor('x') 592 size = check_coor('y') 593 size = check_coor('z') 594 if size == 0: 595 raise ValueError( 596 "'Spatial_map' should contain at least one x, y or z") 597 has_spatial_mapping = True 598 if 'Cartesian_visualisation' in scanning: 599 cv = scanning['Cartesian_visualisation'] 600 if not isinstance(cv, np.ndarray) or cv.ndim != 3: 601 raise ValueError( 602 "Cartesian_visualisation must be a 3D numpy array") 603 if not np.issubdtype(cv.dtype, np.integer) or np.min(cv) < -1 or np.max(cv) >= PSD.shape[0]: 604 raise ValueError( 605 "Cartesian_visualisation values must be integers between -1 and PSD.shape[0]-1") 606 if 'Cartesian_visualisation_pixel' in scanning: 607 if len(scanning['Cartesian_visualisation_pixel']) != 3: 608 raise ValueError( 609 "Cartesian_visualisation_pixel must always contain 3 values for z, y, x (set to None if not used)") 610 else: 611 warnings.warn( 612 "It is recommended to include 'Cartesian_visualisation_pixel' in the scanning dictionary to define pixel size for proper spatial calibration") 613 has_spatial_mapping = True 614 if not has_spatial_mapping and self._sparse: 615 raise ValueError("For sparse data, 'scanning' must be provided and must contain at least one of 'Spatial_map' or 'Cartesian_visualisation'") 616 617 if timestamp is not None: 618 if not isinstance(timestamp, np.ndarray) or timestamp.ndim != 1 or len(timestamp) != PSD.shape[0]: 619 raise ValueError("timestamp is not compatible with PSD") 620 621 # TODO: add and validate additional datasets (i.e. 'Parameters', 'Calibration_index', etc.) 622 623 # Add datasets to the group 624 def determine_chunk_size(arr: np.array) -> tuple: 625 """" 626 Use the same heuristic as the zarr library to determine the chunk size, but without splitting the last dimension 627 """ 628 shape = arr.shape 629 typesize = arr.itemsize 630 #if the array is 1D, do not chunk it 631 if len(shape) <= 1: 632 return (shape[0],) 633 target_sizes = _guess_chunks.__kwdefaults__ 634 # divide the target size by the last dimension size to get the chunk size for the other dimensions 635 target_sizes = {k: target_sizes[k] // shape[-1] 636 for k in target_sizes.keys()} 637 chunks = _guess_chunks(shape[0:-1], typesize, arr.nbytes, **target_sizes) 638 return chunks + (shape[-1],) # keep the last dimension size unchanged 639 sync(self._file.create_dataset( 640 self._group, brim_obj_names.data.PSD, data=PSD, 641 chunk_size=determine_chunk_size(PSD), compression=compression)) 642 freq_ds = sync(self._file.create_dataset( 643 self._group, brim_obj_names.data.frequency, data=frequency, 644 chunk_size=determine_chunk_size(frequency), compression=compression)) 645 units.add_to_object(self._file, freq_ds, freq_units) 646 647 if scanning is not None: 648 if 'Spatial_map' in scanning: 649 sm = scanning['Spatial_map'] 650 sm_group = sync(self._file.create_group(concatenate_paths( 651 self._path, brim_obj_names.data.spatial_map))) 652 if 'units' in sm: 653 units.add_to_object(self._file, sm_group, sm['units']) 654 655 def add_sm_dataset(coord: str): 656 if coord in sm: 657 coord_dts = sync(self._file.create_dataset( 658 sm_group, coord, data=sm[coord], compression=compression)) 659 660 add_sm_dataset('x') 661 add_sm_dataset('y') 662 add_sm_dataset('z') 663 if 'Cartesian_visualisation' in scanning: 664 # convert the Cartesian_visualisation to the smallest integer type 665 cv_arr = np_array_to_smallest_int_type(scanning['Cartesian_visualisation']) 666 cv = sync(self._file.create_dataset(self._group, brim_obj_names.data.cartesian_visualisation, 667 data=cv_arr, compression=compression)) 668 if 'Cartesian_visualisation_pixel' in scanning: 669 sync(self._file.create_attr( 670 cv, 'element_size', scanning['Cartesian_visualisation_pixel'])) 671 if 'Cartesian_visualisation_pixel_unit' in scanning: 672 px_unit = scanning['Cartesian_visualisation_pixel_unit'] 673 else: 674 warnings.warn( 675 "No unit provided for Cartesian_visualisation_pixel, defaulting to 'um'") 676 px_unit = 'um' 677 units.add_to_attribute(self._file, cv, 'element_size', px_unit) 678 679 self._spatial_map, self._spatial_map_px_size = self._load_spatial_mapping() 680 681 if timestamp is not None: 682 sync(self._file.create_dataset( 683 self._group, 'Timestamp', data=timestamp, compression=compression)) 684 685 @staticmethod 686 def list_data_groups(file: FileAbstraction, retrieve_custom_name=False) -> list: 687 """ 688 List all data groups in the brim file. The list is ordered by index. 689 690 Returns: 691 list: A list of dictionaries, each containing: 692 - 'name' (str): The name of the data group in the file. 693 - 'index' (int): The index extracted from the group name. 694 - 'custom_name' (str, optional): if retrieve_custom_name==True, it contains the name of the data group as returned from utils.get_object_name. 695 """ 696 697 data_groups = [] 698 699 matched_objs = list_objects_matching_pattern( 700 file, brim_obj_names.Brillouin_base_path, brim_obj_names.data.base_group + r"_(\d+)$") 701 702 async def _make_dict_item(matched_obj, retrieve_custom_name): 703 name = matched_obj[0] 704 index = int(matched_obj[1]) 705 curr_obj_dict = {'name': name, 'index': index} 706 if retrieve_custom_name: 707 path = concatenate_paths( 708 brim_obj_names.Brillouin_base_path, name) 709 custom_name = await get_object_name(file, path) 710 curr_obj_dict['custom_name'] = custom_name 711 return curr_obj_dict 712 713 coros = [_make_dict_item(matched_obj, retrieve_custom_name) for matched_obj in matched_objs] 714 dicts = _gather_sync(*coros) 715 for dict_item in dicts: 716 data_groups.append(dict_item) 717 # Sort the data groups by index 718 data_groups.sort(key=lambda x: x['index']) 719 720 return data_groups 721 722 @staticmethod 723 def _get_existing_group_name(file: FileAbstraction, index: int) -> str: 724 """ 725 Get the name of an existing data group by index. 726 727 Args: 728 file (File): The parent File object. 729 index (int): The index of the data group. 730 731 Returns: 732 str: The name of the data group, or None if not found. 733 """ 734 group_name: str = None 735 data_groups = Data.list_data_groups(file) 736 for dg in data_groups: 737 if dg['index'] == index: 738 group_name = dg['name'] 739 break 740 return group_name 741 742 @classmethod 743 def _create_new(cls, file: FileAbstraction, index: int, sparse: bool = False, name: str = None) -> 'Data': 744 """ 745 Create a new data group with the specified index. 746 747 Args: 748 file (File): The parent File object. 749 index (int): The index for the new data group. 750 sparse (bool): Whether the data is sparse. See https://github.com/prevedel-lab/Brillouin-standard-file/blob/main/docs/brim_file_specs.md for details. Defaults to False. 751 name (str, optional): The name for the new data group. Defaults to None. 752 753 Returns: 754 Data: The newly created Data object. 755 """ 756 group_name = Data._generate_group_name(index) 757 group = sync(file.create_group(concatenate_paths( 758 brim_obj_names.Brillouin_base_path, group_name))) 759 sync(file.create_attr(group, 'Sparse', sparse)) 760 if name is not None: 761 set_object_name(file, group, name) 762 return cls(file, concatenate_paths(brim_obj_names.Brillouin_base_path, group_name), newly_created=True) 763 764 @staticmethod 765 def _generate_group_name(index: int, n_digits: int = None) -> str: 766 """ 767 Generate a name for a data group based on the index. 768 769 Args: 770 index (int): The index for the data group. 771 n_digits (int, optional): The number of digits to pad the index with. If None no padding is applied. Defaults to None. 772 773 Returns: 774 str: The generated group name. 775 776 Raises: 777 ValueError: If the index is negative. 778 """ 779 if index < 0: 780 raise ValueError("index must be positive") 781 num = str(index) 782 if n_digits is not None: 783 num = num.zfill(n_digits) 784 return f"{brim_obj_names.data.base_group}_{num}"
22class Data: 23 """ 24 Represents a data group within the brim file. 25 """ 26 # make AnalysisResults available as an attribute of Data 27 AnalysisResults = AnalysisResults 28 29 def __init__(self, file: FileAbstraction, path: str, *, newly_created = False): 30 """ 31 Initialize the Data object. This constructor should not be called directly. 32 33 Args: 34 file (File): The parent File object. 35 path (str): The path to the data group within the file. 36 newly_created (bool): Whether this data group is being created as new. 37 If True, the constructor will not attempt to load spatial mapping. 38 """ 39 self._file = file 40 self._path = path 41 self._group = sync(file.open_group(path)) 42 43 self._sparse = self._load_sparse_flag() 44 # the _spatial_map is None for non sparse data but the _spatial_map_px_size should always be valid 45 self._spatial_map, self._spatial_map_px_size = self._load_spatial_mapping() if not newly_created else (None, None) 46 47 def get_name(self): 48 """ 49 Returns the name of the data group. 50 """ 51 return sync(get_object_name(self._file, self._path)) 52 53 def get_index(self): 54 """ 55 Returns the index of the data group. 56 """ 57 return int(self._path.split('/')[-1].split('_')[-1]) 58 59 def _load_sparse_flag(self) -> bool: 60 """ 61 Load the 'Sparse' flag for the data group. 62 63 Returns: 64 bool: The value of the 'Sparse' flag, or False if the attribute is not found or invalid. 65 """ 66 try: 67 sparse = sync(self._file.get_attr(self._group, 'Sparse')) 68 if isinstance(sparse, bool): 69 return sparse 70 else: 71 warnings.warn( 72 f"Invalid value for 'Sparse' attribute in {self._path}. Expected a boolean, got {type(sparse)}. Defaulting to False.") 73 return False 74 except Exception: 75 # if the attribute is not found, return the default value False 76 return False 77 78 def _load_spatial_mapping(self, load_in_memory: bool=True) -> tuple: 79 """ 80 Load a spatial mapping in the same format as 'Cartesian visualisation', 81 irrespectively on whether 'Spatial_map' is defined instead. 82 -1 is used for "empty" pixels in the image 83 Args: 84 load_in_memory (bool): Specify whether the map should be forced to load in memory or just opened as a dataset. 85 Returns: 86 The spatial map and the corresponding pixel size as a tuple of 3 Metadata.Item, both in the order z, y, x. 87 If the spatial mapping is not defined in the file, returns None for the spatial map. 88 The pixel size is read from the data group for non-sparse data. 89 """ 90 cv = None 91 px_size = 3*(Metadata.Item(value=1, units=None),) 92 93 cv_path = concatenate_paths( 94 self._path, brim_obj_names.data.cartesian_visualisation) 95 sm_path = concatenate_paths( 96 self._path, brim_obj_names.data.spatial_map) 97 98 if sync(self._file.object_exists(cv_path)): 99 cv = sync(self._file.open_dataset(cv_path)) 100 101 #read the pixel size from the 'Cartesian visualisation' dataset 102 px_size_val = None 103 px_size_units = None 104 try: 105 px_size_val = sync(self._file.get_attr(cv, 'element_size')) 106 if px_size_val is None or len(px_size_val) != 3: 107 raise ValueError( 108 "The 'element_size' attribute of 'Cartesian_visualisation' must be a tuple of 3 elements") 109 except Exception: 110 px_size_val = 3*(1,) 111 warnings.warn( 112 "No pixel size defined for Cartesian visualisation") 113 px_size_units = sync(units.of_attribute( 114 self._file, cv, 'element_size')) 115 px_size = () 116 for i in range(3): 117 # if px_size_val[i] is not a number, set it to 1 and px_size_units to None 118 if isinstance(px_size_val[i], Number): 119 px_size += (Metadata.Item(px_size_val[i], px_size_units), ) 120 else: 121 px_size += (Metadata.Item(1, None), ) 122 123 124 if load_in_memory: 125 cv = np.array(cv) 126 cv = np_array_to_smallest_int_type(cv) 127 128 elif sync(self._file.object_exists(sm_path)): 129 def load_spatial_map_from_file(self): 130 async def load_coordinate_from_sm(coord: str): 131 res = np.empty(0) # empty array 132 try: 133 res = await self._file.open_dataset( 134 concatenate_paths(sm_path, coord)) 135 res = await res.to_np_array() 136 res = np.squeeze(res) # remove single-dimensional entries 137 except Exception as e: 138 # if the coordinate does not exist, return an empty array 139 pass 140 if len(res.shape) > 1: 141 raise ValueError( 142 f"The 'Spatial_map/{coord}' dataset is not a 1D array as expected") 143 return res 144 145 def check_coord_array(arr, size): 146 if arr.size == 0: 147 return np.zeros(size) 148 elif arr.size != size: 149 raise ValueError( 150 "The 'Spatial_map' dataset is invalid") 151 return arr 152 153 x, y, z = _gather_sync( 154 load_coordinate_from_sm('x'), 155 load_coordinate_from_sm('y'), 156 load_coordinate_from_sm('z') 157 ) 158 size = max([x.size, y.size, z.size]) 159 if size == 0: 160 raise ValueError("The 'Spatial_map' dataset is empty") 161 x = check_coord_array(x, size) 162 y = check_coord_array(y, size) 163 z = check_coord_array(z, size) 164 return x, y, z 165 166 def calculate_step(x): 167 n = len(np.unique(x)) 168 if n == 1: 169 d = None 170 else: 171 d = (np.max(x)-np.min(x))/(n-1) 172 return n, d 173 174 x, y, z = load_spatial_map_from_file(self) 175 176 # TODO extend the reconstruction to non-cartesian cases 177 178 nX, dX = calculate_step(x) 179 nY, dY = calculate_step(y) 180 nZ, dZ = calculate_step(z) 181 182 indices = np_array_to_smallest_int_type(np.lexsort((x, y, z))) 183 cv = np.reshape(indices, (nZ, nY, nX)) 184 185 px_size_units = sync(units.of_object(self._file, sm_path)) 186 px_size = () 187 for i in range(3): 188 px_sz = (dZ, dY, dX)[i] 189 px_unit = px_size_units 190 if px_sz is None: 191 px_sz = 1 192 px_unit = None 193 px_size += (Metadata.Item(px_sz, px_unit),) 194 elif not self._sparse: 195 try: 196 px_sz = sync(self._file.get_attr(self._group, 'element_size')) 197 if len(px_sz) != 3: 198 raise ValueError( 199 "The 'element_size' attribute must be a tuple of 3 elements") 200 px_unit = None 201 try: 202 px_unit = sync(units.of_attribute(self._file, self._group, 'element_size')) 203 except Exception: 204 warnings.warn("Pixel size unit is not provided for non-sparse data.") 205 px_size = tuple(Metadata.Item(el, px_unit) for el in px_sz) 206 except Exception: 207 warnings.warn("Pixel size is not provided for non-sparse data.") 208 209 return cv, px_size 210 211 def get_PSD(self) -> tuple: 212 """ 213 LOW LEVEL FUNCTION 214 215 Retrieve the Power Spectral Density (PSD) and frequency from the current data group. 216 Note: this function exposes the internals of the brim file and thus the interface might change in future versions. 217 Use only if more specialized functions are not working for your application! 218 Returns: 219 tuple: (PSD, frequency, PSD_units, frequency_units) 220 - 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). 221 - 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). 222 - PSD_units: The units of the PSD. 223 - frequency_units: The units of the frequency. 224 """ 225 warnings.warn( 226 "Data.get_PSD is deprecated and will be removed in a future release. " 227 "Use Data.get_PSD_as_spatial_map instead.", 228 DeprecationWarning, 229 stacklevel=2, 230 ) 231 PSD, frequency = _gather_sync( 232 self._file.open_dataset(concatenate_paths( 233 self._path, brim_obj_names.data.PSD)), 234 self._file.open_dataset(concatenate_paths( 235 self._path, brim_obj_names.data.frequency)) 236 ) 237 # retrieve the units of the PSD and frequency 238 PSD_units, frequency_units = _gather_sync( 239 units.of_object(self._file, PSD), 240 units.of_object(self._file, frequency) 241 ) 242 243 return PSD, frequency, PSD_units, frequency_units 244 245 def get_PSD_as_spatial_map(self) -> tuple: 246 """ 247 Retrieve the Power Spectral Density (PSD) as a spatial map and the frequency from the current data group. 248 Returns: 249 tuple: (PSD, frequency, PSD_units, frequency_units) 250 - PSD: A 4D (or more) numpy array containing all the spectra. Dimensions are z, y, x, [parameters], spectrum. 251 - frequency: A numpy array representing the frequency data, which can be broadcastable to PSD. 252 - PSD_units: The units of the PSD. 253 - frequency_units: The units of the frequency. 254 """ 255 PSD, frequency = _gather_sync( 256 self._file.open_dataset(concatenate_paths( 257 self._path, brim_obj_names.data.PSD)), 258 self._file.open_dataset(concatenate_paths( 259 self._path, brim_obj_names.data.frequency)) 260 ) 261 # retrieve the units of the PSD and frequency 262 PSD_units, frequency_units = _gather_sync( 263 units.of_object(self._file, PSD), 264 units.of_object(self._file, frequency) 265 ) 266 267 # ensure PSD and frequency are numpy arrays 268 PSD = np.array(PSD) 269 frequency = np.array(frequency) # ensure it's a numpy array 270 271 #if the frequency is not the same for all spectra, broadcast it to match the shape of PSD 272 if frequency.ndim > 1: 273 frequency = np.broadcast_to(frequency, PSD.shape) 274 275 if self._sparse: 276 if self._spatial_map is None: 277 raise ValueError("The data is defined as sparse, but no spatial mapping is provided.") 278 sm = np.array(self._spatial_map) 279 # reshape the PSD to have the spatial dimensions first 280 PSD = PSD[sm, ...] 281 # reshape the frequency only if it is not the same for all spectra 282 if frequency.ndim > 1: 283 frequency = frequency[sm, ...] 284 285 return PSD, frequency, PSD_units, frequency_units 286 287 def _get_spectrum(self, index: int | tuple[int, int, int]) -> tuple: 288 """ 289 Synchronous wrapper for `_get_spectrum_async` (see doc for `brimfile.data.Data._get_spectrum_async`) 290 """ 291 return sync(self._get_spectrum_async(index)) 292 async def _get_spectrum_async(self, index: int | tuple[int, int, int]) -> tuple: 293 """ 294 Retrieve a spectrum from the data group by its index or coordinates. 295 296 Args: 297 index (int | tuple[int, int, int]): The index (for sparse data) or z, y, x coordinates (for non-sparse data) of the spectrum to retrieve. 298 299 Returns: 300 tuple: (PSD, frequency, PSD_units, frequency_units) for the specified index. 301 PSD can be 1D or more (if there are additional parameters); 302 frequency has the same size as PSD 303 Raises: 304 IndexError: If the index is out of range for the PSD dataset. 305 """ 306 if self._sparse and not isinstance(index, int): 307 raise ValueError("For sparse data, index must be an integer.") 308 elif not self._sparse and not (isinstance(index, tuple) and len(index) == 3): 309 raise ValueError("For non-sparse data, index must be a tuple of (z, y, x) coordinates.") 310 311 # index = -1 corresponds to no spectrum 312 if self._sparse and index < 0: 313 return None, None, None, None 314 elif not self._sparse and any(i < 0 for i in index): 315 return None, None, None, None 316 PSD, frequency = await asyncio.gather( 317 self._file.open_dataset(concatenate_paths( 318 self._path, brim_obj_names.data.PSD)), 319 self._file.open_dataset(concatenate_paths( 320 self._path, brim_obj_names.data.frequency)) 321 ) 322 if self._sparse and index >= PSD.shape[0]: 323 raise IndexError( 324 f"index {index} out of range for PSD with shape {PSD.shape}") 325 elif not self._sparse and any(i >= PSD.shape[j] for j, i in enumerate(index)): 326 raise IndexError( 327 f"index {index} out of range for PSD with shape {PSD.shape}") 328 # retrieve the units of the PSD and frequency 329 PSD_units, frequency_units = await asyncio.gather( 330 units.of_object(self._file, PSD), 331 units.of_object(self._file, frequency) 332 ) 333 # add ellipsis to the index to select the spectrum and the corresponding frequency 334 if self._sparse: 335 index = (index, ...) 336 else: 337 index = index + (..., ) 338 # map index to the frequency array, considering the broadcasting rules 339 index_frequency = index 340 if frequency.ndim < PSD.ndim: 341 if self._sparse: 342 # given the definition of the brim file format, 343 # if the frequency has less dimensions that PSD, 344 # it can only be because it is the same for all the spatial position (first dimension) 345 index_frequency = (..., ) 346 else: 347 unassigned_indices = PSD.ndim - frequency.ndim 348 if unassigned_indices == 3: 349 # if the frequency has no spatial dimension, it is the same for all the spatial positions 350 index_frequency = (..., ) 351 else: 352 # if the frequency has some spatial dimensions but not all, we need to add the corresponding indices to the index of the frequency 353 index_frequency = index[-unassigned_indices:] + (..., ) 354 #get the spectrum and the corresponding frequency at the specified index 355 PSD, frequency = await asyncio.gather( 356 _async_getitem(PSD, index), 357 _async_getitem(frequency, index_frequency) 358 ) 359 #broadcast the frequency to match the shape of PSD if needed 360 if frequency.ndim < PSD.ndim: 361 frequency = np.broadcast_to(frequency, PSD.shape) 362 return PSD, frequency, PSD_units, frequency_units 363 364 def get_spectrum_in_image(self, coor: tuple) -> tuple: 365 """ 366 Retrieve a spectrum from the data group using spatial coordinates. 367 368 Args: 369 coor (tuple): A tuple containing the z, y, x coordinates of the spectrum to retrieve. 370 371 Returns: 372 tuple: A tuple containing the PSD, frequency, PSD_units, frequency_units for the specified coordinates. See `Data._get_spectrum_async` for details. 373 """ 374 if len(coor) != 3: 375 raise ValueError("coor must contain 3 values for z, y, x") 376 377 if self._sparse: 378 index = int(self._spatial_map[coor]) 379 return self._get_spectrum(index) 380 else: 381 return self._get_spectrum(coor) 382 383 def get_spectrum_and_all_quantities_in_image(self, ar: 'Data.AnalysisResults', coor: tuple, index_peak: int = 0): 384 """ 385 Retrieve the spectrum and all available quantities from the analysis results at a specific spatial coordinate. 386 387 Args: 388 ar (Data.AnalysisResults): The analysis results object to retrieve quantities from. 389 coor (tuple): A tuple containing the z, y, x coordinates in the image. 390 index_peak (int, optional): The index of the peak to retrieve (for multi-peak fits). Defaults to 0. 391 392 Returns: 393 tuple: A tuple containing: 394 - spectrum (tuple): (PSD, frequency, PSD_units, frequency_units) at the specified coordinate 395 - quantities (dict): Dictionary of Metadata.Item in the form result[quantity.name][peak.name] 396 """ 397 if len(coor) != 3: 398 raise ValueError("coor must contain 3 values for z, y, x") 399 index = coor 400 if self._sparse: 401 index = int(self._spatial_map[coor]) 402 spectrum, quantities = _gather_sync( 403 self._get_spectrum_async(index), 404 ar._get_all_quantities_at_index(index, index_peak) 405 ) 406 return spectrum, quantities 407 408 def get_metadata(self): 409 """ 410 Returns the metadata associated with the current Data group 411 Note that this contains both the general metadata stored in the file (which might be redifined by the specific data group) 412 and the ones specific for this data group 413 """ 414 return Metadata(self._file, self._path) 415 416 def get_num_parameters(self) -> tuple: 417 """ 418 Retrieves the number of parameters 419 420 Returns: 421 tuple: The shape of the parameters if they exist, otherwise an empty tuple. 422 """ 423 pars, _ = self.get_parameters() 424 return pars.shape if pars is not None else () 425 426 def get_parameters(self) -> list: 427 """ 428 Retrieves the parameters and their associated names. 429 430 If PSD.ndims > 2, the parameters are stored in a separate dataset. 431 432 Returns: 433 list: A tuple containing the parameters and their names if there are any, otherwise None. 434 """ 435 pars_full_path = concatenate_paths( 436 self._path, brim_obj_names.data.parameters) 437 if sync(self._file.object_exists(pars_full_path)): 438 pars = sync(self._file.open_dataset(pars_full_path)) 439 pars_names = sync(self._file.get_attr(pars, 'Name')) 440 return (pars, pars_names) 441 return (None, None) 442 443 def create_analysis_results_group(self, data_AntiStokes, data_Stokes=None, *, 444 index: int = None, name: str = None, fit_model: 'Data.AnalysisResults.FitModel' = None) -> AnalysisResults: 445 """ 446 Adds a new AnalysisResults entry to the current data group. 447 Parameters: 448 data_AntiStokes (dict or list[dict]): see documentation for `brimfile.analysis_results.AnalysisResults.add_data` 449 data_Stokes (dict or list[dict]): same as data_AntiStokes for the Stokes peaks. 450 index (int, optional): The index for the new data entry. If None, the next available index is used. Defaults to None. 451 name (str, optional): The name for the new Analysis group. Defaults to None. 452 fit_model (Data.AnalysisResults.FitModel, optional): The fit model used for the analysis. Defaults to None (no attribute is set). 453 Returns: 454 AnalysisResults: The newly created AnalysisResults object. 455 Raises: 456 IndexError: If the specified index already exists in the dataset. 457 ValueError: If any of the data provided is not valid or consistent 458 """ 459 if index is not None: 460 try: 461 self.get_analysis_results(index) 462 except IndexError: 463 pass 464 else: 465 # If the group already exists, raise an error 466 raise IndexError( 467 f"Analysis {index} already exists in {self._path}") 468 else: 469 ar_groups = self.list_AnalysisResults() 470 indices = [ar['index'] for ar in ar_groups] 471 indices.sort() 472 index = indices[-1] + 1 if indices else 0 # Next available index 473 474 ar = Data.AnalysisResults._create_new(self, index=index, sparse=self._sparse) 475 if name is not None: 476 set_object_name(self._file, ar._path, name) 477 ar.add_data(data_AntiStokes, data_Stokes, fit_model=fit_model) 478 479 return ar 480 481 def list_AnalysisResults(self, retrieve_custom_name=False) -> list: 482 """ 483 List all AnalysisResults groups in the current data group. The list is ordered by index. 484 485 Returns: 486 list: A list of dictionaries, each containing: 487 - 'name' (str): The name of the AnalysisResults group. 488 - 'index' (int): The index extracted from the group name. 489 - 'custom_name' (str, optional): if retrieve_custom_name==True, it contains the name of the AnalysisResults group as returned from utils.get_object_name. 490 """ 491 492 analysis_results_groups = [] 493 494 matched_objs = list_objects_matching_pattern( 495 self._file, self._group, brim_obj_names.data.analysis_results + r"_(\d+)$") 496 async def _make_dict_item(matched_obj, retrieve_custom_name): 497 name = matched_obj[0] 498 index = int(matched_obj[1]) 499 curr_obj_dict = {'name': name, 'index': index} 500 if retrieve_custom_name: 501 ar_path = concatenate_paths(self._path, name) 502 custom_name = await get_object_name(self._file, ar_path) 503 curr_obj_dict['custom_name'] = custom_name 504 return curr_obj_dict 505 coros = [_make_dict_item(matched_obj, retrieve_custom_name) for matched_obj in matched_objs] 506 dicts = _gather_sync(*coros) 507 for dict_item in dicts: 508 analysis_results_groups.append(dict_item) 509 # Sort the data groups by index 510 analysis_results_groups.sort(key=lambda x: x['index']) 511 512 return analysis_results_groups 513 514 def get_analysis_results(self, index: int = 0) -> AnalysisResults: 515 """ 516 Returns the AnalysisResults at the specified index 517 518 Args: 519 index (int) 520 521 Raises: 522 IndexError: If there is no analysis with the corresponding index 523 """ 524 name = None 525 ls = self.list_AnalysisResults() 526 for el in ls: 527 if el['index'] == index: 528 name = el['name'] 529 break 530 if name is None: 531 raise IndexError(f"Analysis {index} not found") 532 path = concatenate_paths(self._path, name) 533 return Data.AnalysisResults(self._file, path, data_group_path=self._path, 534 spatial_map=self._spatial_map, spatial_map_px_size=self._spatial_map_px_size, sparse=self._sparse) 535 536 def _add_data(self, PSD: np.ndarray, frequency: np.ndarray, *, scanning: dict = None, freq_units='GHz', 537 timestamp: np.ndarray = None, compression: FileAbstraction.Compression = FileAbstraction.Compression()): 538 """ 539 Add data to the current data group. 540 541 This method adds the provided PSD, frequency, and scanning data to the HDF5 group 542 associated with this `Data` object. It validates the inputs to ensure they meet 543 the required specifications before adding them. 544 545 Args: 546 PSD (np.ndarray): A 2D numpy array representing the Power Spectral Density (PSD) data. The last dimension contains the spectra. 547 frequency (np.ndarray): A 1D or 2D numpy array representing the frequency data. 548 It must be broadcastable to the shape of the PSD array. 549 scanning (dict, optional): A dictionary containing scanning-related data. 550 Required for sparse data (sparse=True), optional for non-sparse data. 551 For sparse data, must include at least one of 'Spatial_map' or 'Cartesian_visualisation'. 552 It may include the following keys: 553 - 'Spatial_map' (optional): A dictionary containing coordinate arrays: 554 - 'x', 'y', 'z' (optional): 1D numpy arrays of same length with coordinate values 555 - 'units' (optional): string with the unit (e.g., 'um') 556 - 'Cartesian_visualisation' (optional): A 3D numpy array (z, y, x) with integer values 557 mapping spatial positions to spectra indices. Values must be -1 (invalid/empty pixel) 558 or between 0 and PSD.shape[0]-1. 559 - 'Cartesian_visualisation_pixel' (recommended with Cartesian_visualisation): 560 Tuple/list of 3 float values (z, y, x) representing pixel size. Unused dimensions can be None. 561 - 'Cartesian_visualisation_pixel_unit' (optional): String for pixel size unit (default: 'um'). 562 timestamp (np.ndarray, optional): Timestamps in milliseconds for each spectrum. 563 Must be a 1D array with length equal to PSD.shape[0]. 564 565 566 Raises: 567 ValueError: If any of the data provided is not valid or consistent 568 """ 569 570 # Check if frequency is broadcastable to PSD 571 try: 572 np.broadcast_shapes(tuple(frequency.shape), tuple(PSD.shape)) 573 except ValueError as e: 574 raise ValueError(f"frequency (shape: {frequency.shape}) is not broadcastable to PSD (shape: {PSD.shape}): {e}") 575 576 # Check if at least one of 'Spatial_map' or 'Cartesian_visualisation' is present in the scanning dictionary 577 # This is required for sparse data to establish the spatial mapping 578 has_spatial_mapping = False 579 if scanning is not None: 580 if 'Spatial_map' in scanning: 581 sm = scanning['Spatial_map'] 582 size = 0 583 584 def check_coor(coor: str): 585 if coor in sm: 586 sm[coor] = np.array(sm[coor]) 587 size1 = sm[coor].size 588 if size1 != size and size != 0: 589 raise ValueError( 590 f"'{coor}' in 'Spatial_map' is invalid!") 591 return size1 592 size = check_coor('x') 593 size = check_coor('y') 594 size = check_coor('z') 595 if size == 0: 596 raise ValueError( 597 "'Spatial_map' should contain at least one x, y or z") 598 has_spatial_mapping = True 599 if 'Cartesian_visualisation' in scanning: 600 cv = scanning['Cartesian_visualisation'] 601 if not isinstance(cv, np.ndarray) or cv.ndim != 3: 602 raise ValueError( 603 "Cartesian_visualisation must be a 3D numpy array") 604 if not np.issubdtype(cv.dtype, np.integer) or np.min(cv) < -1 or np.max(cv) >= PSD.shape[0]: 605 raise ValueError( 606 "Cartesian_visualisation values must be integers between -1 and PSD.shape[0]-1") 607 if 'Cartesian_visualisation_pixel' in scanning: 608 if len(scanning['Cartesian_visualisation_pixel']) != 3: 609 raise ValueError( 610 "Cartesian_visualisation_pixel must always contain 3 values for z, y, x (set to None if not used)") 611 else: 612 warnings.warn( 613 "It is recommended to include 'Cartesian_visualisation_pixel' in the scanning dictionary to define pixel size for proper spatial calibration") 614 has_spatial_mapping = True 615 if not has_spatial_mapping and self._sparse: 616 raise ValueError("For sparse data, 'scanning' must be provided and must contain at least one of 'Spatial_map' or 'Cartesian_visualisation'") 617 618 if timestamp is not None: 619 if not isinstance(timestamp, np.ndarray) or timestamp.ndim != 1 or len(timestamp) != PSD.shape[0]: 620 raise ValueError("timestamp is not compatible with PSD") 621 622 # TODO: add and validate additional datasets (i.e. 'Parameters', 'Calibration_index', etc.) 623 624 # Add datasets to the group 625 def determine_chunk_size(arr: np.array) -> tuple: 626 """" 627 Use the same heuristic as the zarr library to determine the chunk size, but without splitting the last dimension 628 """ 629 shape = arr.shape 630 typesize = arr.itemsize 631 #if the array is 1D, do not chunk it 632 if len(shape) <= 1: 633 return (shape[0],) 634 target_sizes = _guess_chunks.__kwdefaults__ 635 # divide the target size by the last dimension size to get the chunk size for the other dimensions 636 target_sizes = {k: target_sizes[k] // shape[-1] 637 for k in target_sizes.keys()} 638 chunks = _guess_chunks(shape[0:-1], typesize, arr.nbytes, **target_sizes) 639 return chunks + (shape[-1],) # keep the last dimension size unchanged 640 sync(self._file.create_dataset( 641 self._group, brim_obj_names.data.PSD, data=PSD, 642 chunk_size=determine_chunk_size(PSD), compression=compression)) 643 freq_ds = sync(self._file.create_dataset( 644 self._group, brim_obj_names.data.frequency, data=frequency, 645 chunk_size=determine_chunk_size(frequency), compression=compression)) 646 units.add_to_object(self._file, freq_ds, freq_units) 647 648 if scanning is not None: 649 if 'Spatial_map' in scanning: 650 sm = scanning['Spatial_map'] 651 sm_group = sync(self._file.create_group(concatenate_paths( 652 self._path, brim_obj_names.data.spatial_map))) 653 if 'units' in sm: 654 units.add_to_object(self._file, sm_group, sm['units']) 655 656 def add_sm_dataset(coord: str): 657 if coord in sm: 658 coord_dts = sync(self._file.create_dataset( 659 sm_group, coord, data=sm[coord], compression=compression)) 660 661 add_sm_dataset('x') 662 add_sm_dataset('y') 663 add_sm_dataset('z') 664 if 'Cartesian_visualisation' in scanning: 665 # convert the Cartesian_visualisation to the smallest integer type 666 cv_arr = np_array_to_smallest_int_type(scanning['Cartesian_visualisation']) 667 cv = sync(self._file.create_dataset(self._group, brim_obj_names.data.cartesian_visualisation, 668 data=cv_arr, compression=compression)) 669 if 'Cartesian_visualisation_pixel' in scanning: 670 sync(self._file.create_attr( 671 cv, 'element_size', scanning['Cartesian_visualisation_pixel'])) 672 if 'Cartesian_visualisation_pixel_unit' in scanning: 673 px_unit = scanning['Cartesian_visualisation_pixel_unit'] 674 else: 675 warnings.warn( 676 "No unit provided for Cartesian_visualisation_pixel, defaulting to 'um'") 677 px_unit = 'um' 678 units.add_to_attribute(self._file, cv, 'element_size', px_unit) 679 680 self._spatial_map, self._spatial_map_px_size = self._load_spatial_mapping() 681 682 if timestamp is not None: 683 sync(self._file.create_dataset( 684 self._group, 'Timestamp', data=timestamp, compression=compression)) 685 686 @staticmethod 687 def list_data_groups(file: FileAbstraction, retrieve_custom_name=False) -> list: 688 """ 689 List all data groups in the brim file. The list is ordered by index. 690 691 Returns: 692 list: A list of dictionaries, each containing: 693 - 'name' (str): The name of the data group in the file. 694 - 'index' (int): The index extracted from the group name. 695 - 'custom_name' (str, optional): if retrieve_custom_name==True, it contains the name of the data group as returned from utils.get_object_name. 696 """ 697 698 data_groups = [] 699 700 matched_objs = list_objects_matching_pattern( 701 file, brim_obj_names.Brillouin_base_path, brim_obj_names.data.base_group + r"_(\d+)$") 702 703 async def _make_dict_item(matched_obj, retrieve_custom_name): 704 name = matched_obj[0] 705 index = int(matched_obj[1]) 706 curr_obj_dict = {'name': name, 'index': index} 707 if retrieve_custom_name: 708 path = concatenate_paths( 709 brim_obj_names.Brillouin_base_path, name) 710 custom_name = await get_object_name(file, path) 711 curr_obj_dict['custom_name'] = custom_name 712 return curr_obj_dict 713 714 coros = [_make_dict_item(matched_obj, retrieve_custom_name) for matched_obj in matched_objs] 715 dicts = _gather_sync(*coros) 716 for dict_item in dicts: 717 data_groups.append(dict_item) 718 # Sort the data groups by index 719 data_groups.sort(key=lambda x: x['index']) 720 721 return data_groups 722 723 @staticmethod 724 def _get_existing_group_name(file: FileAbstraction, index: int) -> str: 725 """ 726 Get the name of an existing data group by index. 727 728 Args: 729 file (File): The parent File object. 730 index (int): The index of the data group. 731 732 Returns: 733 str: The name of the data group, or None if not found. 734 """ 735 group_name: str = None 736 data_groups = Data.list_data_groups(file) 737 for dg in data_groups: 738 if dg['index'] == index: 739 group_name = dg['name'] 740 break 741 return group_name 742 743 @classmethod 744 def _create_new(cls, file: FileAbstraction, index: int, sparse: bool = False, name: str = None) -> 'Data': 745 """ 746 Create a new data group with the specified index. 747 748 Args: 749 file (File): The parent File object. 750 index (int): The index for the new data group. 751 sparse (bool): Whether the data is sparse. See https://github.com/prevedel-lab/Brillouin-standard-file/blob/main/docs/brim_file_specs.md for details. Defaults to False. 752 name (str, optional): The name for the new data group. Defaults to None. 753 754 Returns: 755 Data: The newly created Data object. 756 """ 757 group_name = Data._generate_group_name(index) 758 group = sync(file.create_group(concatenate_paths( 759 brim_obj_names.Brillouin_base_path, group_name))) 760 sync(file.create_attr(group, 'Sparse', sparse)) 761 if name is not None: 762 set_object_name(file, group, name) 763 return cls(file, concatenate_paths(brim_obj_names.Brillouin_base_path, group_name), newly_created=True) 764 765 @staticmethod 766 def _generate_group_name(index: int, n_digits: int = None) -> str: 767 """ 768 Generate a name for a data group based on the index. 769 770 Args: 771 index (int): The index for the data group. 772 n_digits (int, optional): The number of digits to pad the index with. If None no padding is applied. Defaults to None. 773 774 Returns: 775 str: The generated group name. 776 777 Raises: 778 ValueError: If the index is negative. 779 """ 780 if index < 0: 781 raise ValueError("index must be positive") 782 num = str(index) 783 if n_digits is not None: 784 num = num.zfill(n_digits) 785 return f"{brim_obj_names.data.base_group}_{num}"
Represents a data group within the brim file.
29 def __init__(self, file: FileAbstraction, path: str, *, newly_created = False): 30 """ 31 Initialize the Data object. This constructor should not be called directly. 32 33 Args: 34 file (File): The parent File object. 35 path (str): The path to the data group within the file. 36 newly_created (bool): Whether this data group is being created as new. 37 If True, the constructor will not attempt to load spatial mapping. 38 """ 39 self._file = file 40 self._path = path 41 self._group = sync(file.open_group(path)) 42 43 self._sparse = self._load_sparse_flag() 44 # the _spatial_map is None for non sparse data but the _spatial_map_px_size should always be valid 45 self._spatial_map, self._spatial_map_px_size = self._load_spatial_mapping() if not newly_created else (None, None)
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.
- newly_created (bool): Whether this data group is being created as new. If True, the constructor will not attempt to load spatial mapping.
47 def get_name(self): 48 """ 49 Returns the name of the data group. 50 """ 51 return sync(get_object_name(self._file, self._path))
Returns the name of the data group.
53 def get_index(self): 54 """ 55 Returns the index of the data group. 56 """ 57 return int(self._path.split('/')[-1].split('_')[-1])
Returns the index of the data group.
211 def get_PSD(self) -> tuple: 212 """ 213 LOW LEVEL FUNCTION 214 215 Retrieve the Power Spectral Density (PSD) and frequency from the current data group. 216 Note: this function exposes the internals of the brim file and thus the interface might change in future versions. 217 Use only if more specialized functions are not working for your application! 218 Returns: 219 tuple: (PSD, frequency, PSD_units, frequency_units) 220 - 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). 221 - 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). 222 - PSD_units: The units of the PSD. 223 - frequency_units: The units of the frequency. 224 """ 225 warnings.warn( 226 "Data.get_PSD is deprecated and will be removed in a future release. " 227 "Use Data.get_PSD_as_spatial_map instead.", 228 DeprecationWarning, 229 stacklevel=2, 230 ) 231 PSD, frequency = _gather_sync( 232 self._file.open_dataset(concatenate_paths( 233 self._path, brim_obj_names.data.PSD)), 234 self._file.open_dataset(concatenate_paths( 235 self._path, brim_obj_names.data.frequency)) 236 ) 237 # retrieve the units of the PSD and frequency 238 PSD_units, frequency_units = _gather_sync( 239 units.of_object(self._file, PSD), 240 units.of_object(self._file, frequency) 241 ) 242 243 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.
245 def get_PSD_as_spatial_map(self) -> tuple: 246 """ 247 Retrieve the Power Spectral Density (PSD) as a spatial map and the frequency from the current data group. 248 Returns: 249 tuple: (PSD, frequency, PSD_units, frequency_units) 250 - PSD: A 4D (or more) numpy array containing all the spectra. Dimensions are z, y, x, [parameters], spectrum. 251 - frequency: A numpy array representing the frequency data, which can be broadcastable to PSD. 252 - PSD_units: The units of the PSD. 253 - frequency_units: The units of the frequency. 254 """ 255 PSD, frequency = _gather_sync( 256 self._file.open_dataset(concatenate_paths( 257 self._path, brim_obj_names.data.PSD)), 258 self._file.open_dataset(concatenate_paths( 259 self._path, brim_obj_names.data.frequency)) 260 ) 261 # retrieve the units of the PSD and frequency 262 PSD_units, frequency_units = _gather_sync( 263 units.of_object(self._file, PSD), 264 units.of_object(self._file, frequency) 265 ) 266 267 # ensure PSD and frequency are numpy arrays 268 PSD = np.array(PSD) 269 frequency = np.array(frequency) # ensure it's a numpy array 270 271 #if the frequency is not the same for all spectra, broadcast it to match the shape of PSD 272 if frequency.ndim > 1: 273 frequency = np.broadcast_to(frequency, PSD.shape) 274 275 if self._sparse: 276 if self._spatial_map is None: 277 raise ValueError("The data is defined as sparse, but no spatial mapping is provided.") 278 sm = np.array(self._spatial_map) 279 # reshape the PSD to have the spatial dimensions first 280 PSD = PSD[sm, ...] 281 # reshape the frequency only if it is not the same for all spectra 282 if frequency.ndim > 1: 283 frequency = frequency[sm, ...] 284 285 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.
364 def get_spectrum_in_image(self, coor: tuple) -> tuple: 365 """ 366 Retrieve a spectrum from the data group using spatial coordinates. 367 368 Args: 369 coor (tuple): A tuple containing the z, y, x coordinates of the spectrum to retrieve. 370 371 Returns: 372 tuple: A tuple containing the PSD, frequency, PSD_units, frequency_units for the specified coordinates. See `Data._get_spectrum_async` for details. 373 """ 374 if len(coor) != 3: 375 raise ValueError("coor must contain 3 values for z, y, x") 376 377 if self._sparse: 378 index = int(self._spatial_map[coor]) 379 return self._get_spectrum(index) 380 else: 381 return self._get_spectrum(coor)
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
Data._get_spectrum_asyncfor details.
383 def get_spectrum_and_all_quantities_in_image(self, ar: 'Data.AnalysisResults', coor: tuple, index_peak: int = 0): 384 """ 385 Retrieve the spectrum and all available quantities from the analysis results at a specific spatial coordinate. 386 387 Args: 388 ar (Data.AnalysisResults): The analysis results object to retrieve quantities from. 389 coor (tuple): A tuple containing the z, y, x coordinates in the image. 390 index_peak (int, optional): The index of the peak to retrieve (for multi-peak fits). Defaults to 0. 391 392 Returns: 393 tuple: A tuple containing: 394 - spectrum (tuple): (PSD, frequency, PSD_units, frequency_units) at the specified coordinate 395 - quantities (dict): Dictionary of Metadata.Item in the form result[quantity.name][peak.name] 396 """ 397 if len(coor) != 3: 398 raise ValueError("coor must contain 3 values for z, y, x") 399 index = coor 400 if self._sparse: 401 index = int(self._spatial_map[coor]) 402 spectrum, quantities = _gather_sync( 403 self._get_spectrum_async(index), 404 ar._get_all_quantities_at_index(index, index_peak) 405 ) 406 return spectrum, quantities
Retrieve the spectrum and all available quantities from the analysis results at a specific spatial coordinate.
Arguments:
- ar (Data.AnalysisResults): The analysis results object to retrieve quantities from.
- coor (tuple): A tuple containing the z, y, x coordinates in the image.
- index_peak (int, optional): The index of the peak to retrieve (for multi-peak fits). Defaults to 0.
Returns:
tuple: A tuple containing: - spectrum (tuple): (PSD, frequency, PSD_units, frequency_units) at the specified coordinate - quantities (dict): Dictionary of Metadata.Item in the form result[quantity.name][peak.name]
408 def get_metadata(self): 409 """ 410 Returns the metadata associated with the current Data group 411 Note that this contains both the general metadata stored in the file (which might be redifined by the specific data group) 412 and the ones specific for this data group 413 """ 414 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
416 def get_num_parameters(self) -> tuple: 417 """ 418 Retrieves the number of parameters 419 420 Returns: 421 tuple: The shape of the parameters if they exist, otherwise an empty tuple. 422 """ 423 pars, _ = self.get_parameters() 424 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.
426 def get_parameters(self) -> list: 427 """ 428 Retrieves the parameters and their associated names. 429 430 If PSD.ndims > 2, the parameters are stored in a separate dataset. 431 432 Returns: 433 list: A tuple containing the parameters and their names if there are any, otherwise None. 434 """ 435 pars_full_path = concatenate_paths( 436 self._path, brim_obj_names.data.parameters) 437 if sync(self._file.object_exists(pars_full_path)): 438 pars = sync(self._file.open_dataset(pars_full_path)) 439 pars_names = sync(self._file.get_attr(pars, 'Name')) 440 return (pars, pars_names) 441 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.
443 def create_analysis_results_group(self, data_AntiStokes, data_Stokes=None, *, 444 index: int = None, name: str = None, fit_model: 'Data.AnalysisResults.FitModel' = None) -> AnalysisResults: 445 """ 446 Adds a new AnalysisResults entry to the current data group. 447 Parameters: 448 data_AntiStokes (dict or list[dict]): see documentation for `brimfile.analysis_results.AnalysisResults.add_data` 449 data_Stokes (dict or list[dict]): same as data_AntiStokes for the Stokes peaks. 450 index (int, optional): The index for the new data entry. If None, the next available index is used. Defaults to None. 451 name (str, optional): The name for the new Analysis group. Defaults to None. 452 fit_model (Data.AnalysisResults.FitModel, optional): The fit model used for the analysis. Defaults to None (no attribute is set). 453 Returns: 454 AnalysisResults: The newly created AnalysisResults object. 455 Raises: 456 IndexError: If the specified index already exists in the dataset. 457 ValueError: If any of the data provided is not valid or consistent 458 """ 459 if index is not None: 460 try: 461 self.get_analysis_results(index) 462 except IndexError: 463 pass 464 else: 465 # If the group already exists, raise an error 466 raise IndexError( 467 f"Analysis {index} already exists in {self._path}") 468 else: 469 ar_groups = self.list_AnalysisResults() 470 indices = [ar['index'] for ar in ar_groups] 471 indices.sort() 472 index = indices[-1] + 1 if indices else 0 # Next available index 473 474 ar = Data.AnalysisResults._create_new(self, index=index, sparse=self._sparse) 475 if name is not None: 476 set_object_name(self._file, ar._path, name) 477 ar.add_data(data_AntiStokes, data_Stokes, fit_model=fit_model) 478 479 return ar
Adds a new AnalysisResults entry to the current data group.
Arguments:
- data_AntiStokes (dict or list[dict]): see documentation for
brimfile.analysis_results.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
481 def list_AnalysisResults(self, retrieve_custom_name=False) -> list: 482 """ 483 List all AnalysisResults groups in the current data group. The list is ordered by index. 484 485 Returns: 486 list: A list of dictionaries, each containing: 487 - 'name' (str): The name of the AnalysisResults group. 488 - 'index' (int): The index extracted from the group name. 489 - 'custom_name' (str, optional): if retrieve_custom_name==True, it contains the name of the AnalysisResults group as returned from utils.get_object_name. 490 """ 491 492 analysis_results_groups = [] 493 494 matched_objs = list_objects_matching_pattern( 495 self._file, self._group, brim_obj_names.data.analysis_results + r"_(\d+)$") 496 async def _make_dict_item(matched_obj, retrieve_custom_name): 497 name = matched_obj[0] 498 index = int(matched_obj[1]) 499 curr_obj_dict = {'name': name, 'index': index} 500 if retrieve_custom_name: 501 ar_path = concatenate_paths(self._path, name) 502 custom_name = await get_object_name(self._file, ar_path) 503 curr_obj_dict['custom_name'] = custom_name 504 return curr_obj_dict 505 coros = [_make_dict_item(matched_obj, retrieve_custom_name) for matched_obj in matched_objs] 506 dicts = _gather_sync(*coros) 507 for dict_item in dicts: 508 analysis_results_groups.append(dict_item) 509 # Sort the data groups by index 510 analysis_results_groups.sort(key=lambda x: x['index']) 511 512 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.
514 def get_analysis_results(self, index: int = 0) -> AnalysisResults: 515 """ 516 Returns the AnalysisResults at the specified index 517 518 Args: 519 index (int) 520 521 Raises: 522 IndexError: If there is no analysis with the corresponding index 523 """ 524 name = None 525 ls = self.list_AnalysisResults() 526 for el in ls: 527 if el['index'] == index: 528 name = el['name'] 529 break 530 if name is None: 531 raise IndexError(f"Analysis {index} not found") 532 path = concatenate_paths(self._path, name) 533 return Data.AnalysisResults(self._file, path, data_group_path=self._path, 534 spatial_map=self._spatial_map, spatial_map_px_size=self._spatial_map_px_size, sparse=self._sparse)
Returns the AnalysisResults at the specified index
Arguments:
- index (int)
Raises:
- IndexError: If there is no analysis with the corresponding index
686 @staticmethod 687 def list_data_groups(file: FileAbstraction, retrieve_custom_name=False) -> list: 688 """ 689 List all data groups in the brim file. The list is ordered by index. 690 691 Returns: 692 list: A list of dictionaries, each containing: 693 - 'name' (str): The name of the data group in the file. 694 - 'index' (int): The index extracted from the group name. 695 - 'custom_name' (str, optional): if retrieve_custom_name==True, it contains the name of the data group as returned from utils.get_object_name. 696 """ 697 698 data_groups = [] 699 700 matched_objs = list_objects_matching_pattern( 701 file, brim_obj_names.Brillouin_base_path, brim_obj_names.data.base_group + r"_(\d+)$") 702 703 async def _make_dict_item(matched_obj, retrieve_custom_name): 704 name = matched_obj[0] 705 index = int(matched_obj[1]) 706 curr_obj_dict = {'name': name, 'index': index} 707 if retrieve_custom_name: 708 path = concatenate_paths( 709 brim_obj_names.Brillouin_base_path, name) 710 custom_name = await get_object_name(file, path) 711 curr_obj_dict['custom_name'] = custom_name 712 return curr_obj_dict 713 714 coros = [_make_dict_item(matched_obj, retrieve_custom_name) for matched_obj in matched_objs] 715 dicts = _gather_sync(*coros) 716 for dict_item in dicts: 717 data_groups.append(dict_item) 718 # Sort the data groups by index 719 data_groups.sort(key=lambda x: x['index']) 720 721 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.
23class AnalysisResults: 24 """ 25 Rapresents the analysis results associated with a Data object. 26 """ 27 28 class Quantity(Enum): 29 """ 30 Enum representing the type of analysis results. 31 """ 32 Shift = "Shift" 33 Width = "Width" 34 Amplitude = "Amplitude" 35 Offset = "Offset" 36 R2 = "R2" 37 RMSE = "RMSE" 38 Cov_matrix = "Cov_matrix" 39 40 class PeakType(Enum): 41 AntiStokes = "AS" 42 Stokes = "S" 43 average = "avg" 44 45 class FitModel(Enum): 46 Undefined = "Undefined" 47 Lorentzian = "Lorentzian" 48 DHO = "DHO" 49 Gaussian = "Gaussian" 50 Voigt = "Voigt" 51 Custom = "Custom" 52 53 def __init__(self, file: FileAbstraction, full_path: str, *, data_group_path: str, 54 spatial_map = None, spatial_map_px_size = None, sparse: bool = False): 55 """ 56 Initialize the AnalysisResults object. 57 58 Args: 59 file (File): The parent File object. 60 full_path (str): path of the group storing the analysis results 61 data_group_path (str): path of the data group associated with the analysis results 62 """ 63 self._file = file 64 self._path = full_path 65 self._data_group_path = data_group_path 66 # self._group = file.open_group(full_path) 67 self._spatial_map = spatial_map 68 self._spatial_map_px_size = spatial_map_px_size 69 self._sparse = sparse 70 if sparse: 71 if spatial_map is None or spatial_map_px_size is None: 72 raise ValueError("For sparse analysis results, the spatial map and pixel size must be provided.") 73 def get_name(self): 74 """ 75 Returns the name of the Analysis group. 76 """ 77 return sync(get_object_name(self._file, self._path)) 78 79 @classmethod 80 def _create_new(cls, data: 'Data', *, index: int, sparse: bool = False) -> 'AnalysisResults': 81 """ 82 Create a new AnalysisResults group. 83 84 Args: 85 file (FileAbstraction): The file. 86 index (int): The index for the new AnalysisResults group. 87 88 Returns: 89 AnalysisResults: The newly created AnalysisResults object. 90 """ 91 group_name = f"{brim_obj_names.data.analysis_results}_{index}" 92 ar_full_path = concatenate_paths(data._path, group_name) 93 group = sync(data._file.create_group(ar_full_path)) 94 return cls(data._file, ar_full_path, data_group_path=data._path, 95 spatial_map=data._spatial_map, spatial_map_px_size=data._spatial_map_px_size, 96 sparse=sparse) 97 98 def add_data(self, data_AntiStokes=None, data_Stokes=None, *, 99 fit_model: 'AnalysisResults.FitModel' = None): 100 """ 101 Adds data for the analysis results for AntiStokes and Stokes peaks to the file. 102 103 Args: 104 data_AntiStokes (dict or list[dict]): A dictionary containing the analysis results for AntiStokes peaks. 105 In case multiple peaks were fitted, it might be a list of dictionaries with each element corresponding to a single peak. 106 107 Each dictionary may include the following keys (plus the corresponding units, e.g. 'shift_units'): 108 - 'shift': The shift value. 109 - 'width': The width value. 110 - 'amplitude': The amplitude value. 111 - 'offset': The offset value. 112 - 'R2': The R-squared value. 113 - 'RMSE': The root mean square error value. 114 - 'Cov_matrix': The covariance matrix. 115 The above arrays must have one less dimension than the PSD dataset, with the same shape as the first n-1 dimensions of the PSD (i.e. all the dimensions except the last (spectral) one). 116 The 'Cov_matrix' should have 2 additional last dimensions which define the matrix. 117 data_Stokes (dict or list[dict]): same as `data_AntiStokes` for the Stokes peaks. 118 fit_model (AnalysisResults.FitModel, optional): The fit model used for the analysis. Defaults to None (no attribute is set). 119 120 Both `data_AntiStokes` and `data_Stokes` are optional, but at least one of them must be provided. 121 """ 122 123 ar_cls = self.__class__ 124 ar_group = sync(self._file.open_group(self._path)) 125 126 def add_quantity(qt: AnalysisResults.Quantity, pt: AnalysisResults.PeakType, data, index: int = 0): 127 # PSD_nonspectral_shape is an closure variable that is used to check the shape of the data being added, if the PSD dataset is already present in the current data group. 128 if PSD_nonspectral_shape is not None: 129 expected_shape = PSD_nonspectral_shape 130 if qt is AnalysisResults.Quantity.Cov_matrix: 131 expected_shape += (data.shape[-2], data.shape[-1]) 132 if data.shape != expected_shape: 133 raise ValueError(f"The shape of the '{qt.value}' data is {data.shape}, but it should be {expected_shape} to match the shape of the PSD.") 134 sync(self._file.create_dataset( 135 ar_group, ar_cls._get_quantity_name(qt, pt, index), data)) 136 137 def add_data_pt(pt: AnalysisResults.PeakType, data, index: int = 0): 138 if 'shift' in data: 139 add_quantity(ar_cls.Quantity.Shift, 140 pt, data['shift'], index) 141 if 'shift_units' in data: 142 self._set_units(data['shift_units'], 143 ar_cls.Quantity.Shift, pt, index) 144 if 'width' in data: 145 add_quantity(ar_cls.Quantity.Width, 146 pt, data['width'], index) 147 if 'width_units' in data: 148 self._set_units(data['width_units'], 149 ar_cls.Quantity.Width, pt, index) 150 if 'amplitude' in data: 151 add_quantity(ar_cls.Quantity.Amplitude, 152 pt, data['amplitude'], index) 153 if 'amplitude_units' in data: 154 self._set_units( 155 data['amplitude_units'], ar_cls.Quantity.Amplitude, pt, index) 156 if 'offset' in data: 157 add_quantity(ar_cls.Quantity.Offset, 158 pt, data['offset'], index) 159 if 'offset_units' in data: 160 self._set_units( 161 data['offset_units'], ar_cls.Quantity.Offset, pt, index) 162 if 'R2' in data: 163 add_quantity(ar_cls.Quantity.R2, pt, data['R2'], index) 164 if 'R2_units' in data: 165 self._set_units(data['R2_units'], 166 ar_cls.Quantity.R2, pt, index) 167 if 'RMSE' in data: 168 add_quantity(ar_cls.Quantity.RMSE, pt, data['RMSE'], index) 169 if 'RMSE_units' in data: 170 self._set_units(data['RMSE_units'], 171 ar_cls.Quantity.RMSE, pt, index) 172 if 'Cov_matrix' in data: 173 add_quantity(ar_cls.Quantity.Cov_matrix, 174 pt, data['Cov_matrix'], index) 175 if 'Cov_matrix_units' in data: 176 self._set_units( 177 data['Cov_matrix_units'], ar_cls.Quantity.Cov_matrix, pt, index) 178 179 PSD_nonspectral_shape = None 180 try: 181 PSD = sync(self._file.open_dataset(concatenate_paths( 182 self._data_group_path, brim_obj_names.data.PSD))) 183 PSD_nonspectral_shape = PSD.shape[:-1] 184 except Exception as e: 185 warnings.warn("It is recommended to add the PSD dataset before adding the analysis results, to ensure the correct shape of the analysis results data.") 186 187 if data_AntiStokes is not None: 188 data_AntiStokes = var_to_singleton(data_AntiStokes) 189 for i, d_as in enumerate(data_AntiStokes): 190 add_data_pt(ar_cls.PeakType.AntiStokes, d_as, i) 191 if data_Stokes is not None: 192 data_Stokes = var_to_singleton(data_Stokes) 193 for i, d_s in enumerate(data_Stokes): 194 add_data_pt(ar_cls.PeakType.Stokes, d_s, i) 195 if fit_model is not None: 196 sync(self._file.create_attr(ar_group, 'Fit_model', fit_model.value)) 197 198 def get_units(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0) -> str: 199 """ 200 Retrieve the units of a specified quantity from the data file. 201 202 Args: 203 qt (Quantity): The quantity for which the units are to be retrieved. 204 pt (PeakType, optional): The type of peak (e.g., Stokes or AntiStokes). Defaults to PeakType.AntiStokes. 205 index (int, optional): The index of the quantity in case multiple quantities exist. Defaults to 0. 206 207 Returns: 208 str: The units of the specified quantity as a string. 209 """ 210 dt_name = AnalysisResults._get_quantity_name(qt, pt, index) 211 full_path = concatenate_paths(self._path, dt_name) 212 return sync(units.of_object(self._file, full_path)) 213 214 def _set_units(self, un: str, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0) -> str: 215 """ 216 Set the units of a specified quantity. 217 218 Args: 219 un (str): The units to be set. 220 qt (Quantity): The quantity for which the units are to be set. 221 pt (PeakType, optional): The type of peak (e.g., Stokes or AntiStokes). Defaults to PeakType.AntiStokes. 222 index (int, optional): The index of the quantity in case multiple quantities exist. Defaults to 0. 223 224 Returns: 225 str: The units of the specified quantity as a string. 226 """ 227 dt_name = AnalysisResults._get_quantity_name(qt, pt, index) 228 full_path = concatenate_paths(self._path, dt_name) 229 return units.add_to_object(self._file, full_path, un) 230 231 @property 232 def fit_model(self) -> 'AnalysisResults.FitModel': 233 """ 234 Retrieve the fit model used for the analysis. 235 236 Returns: 237 AnalysisResults.FitModel: The fit model used for the analysis. 238 """ 239 if not hasattr(self, '_fit_model'): 240 try: 241 fit_model_str = sync(self._file.get_attr(self._path, 'Fit_model')) 242 self._fit_model = AnalysisResults.FitModel(fit_model_str) 243 except Exception as e: 244 if isinstance(e, ValueError): 245 warnings.warn( 246 f"Unknown fit model '{fit_model_str}' found in the file.") 247 self._fit_model = AnalysisResults.FitModel.Undefined 248 return self._fit_model 249 250 def save_image_to_OMETiff(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0, filename: str = None) -> str: 251 """ 252 Saves the image corresponding to the specified quantity and index to an OMETiff file. 253 254 Args: 255 qt (Quantity): The quantity to retrieve the image for (e.g. shift). 256 pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes). 257 index (int, optional): The index of the data to retrieve, if multiple are present (default is 0). 258 filename (str, optional): The name of the file to save the image to. If None, a default name will be used. 259 260 Returns: 261 str: The path to the saved OMETiff file. 262 """ 263 try: 264 import tifffile 265 except ImportError: 266 raise ModuleNotFoundError( 267 "The tifffile module is required for saving to OME-Tiff. Please install it using 'pip install tifffile'.") 268 269 if filename is None: 270 filename = f"{qt.value}_{pt.value}_{index}.ome.tif" 271 if not filename.endswith('.ome.tif'): 272 filename += '.ome.tif' 273 img, px_size = self.get_image(qt, pt, index) 274 if img.ndim > 3: 275 raise NotImplementedError( 276 "Saving images with more than 3 dimensions is not supported yet.") 277 with tifffile.TiffWriter(filename, bigtiff=True) as tif: 278 metadata = { 279 'axes': 'ZYX', 280 'PhysicalSizeX': px_size[2].value, 281 'PhysicalSizeXUnit': px_size[2].units, 282 'PhysicalSizeY': px_size[1].value, 283 'PhysicalSizeYUnit': px_size[1].units, 284 'PhysicalSizeZ': px_size[0].value, 285 'PhysicalSizeZUnit': px_size[0].units, 286 } 287 tif.write(img, metadata=metadata) 288 return filename 289 290 def get_image(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0) -> tuple: 291 """ 292 Retrieves an image (spatial map) based on the specified quantity, peak type, and index. 293 294 Args: 295 qt (Quantity): The quantity to retrieve the image for (e.g. shift). 296 pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes). 297 index (int, optional): The index of the data to retrieve, if multiple are present (default is 0). 298 299 Returns: 300 A tuple containing the image corresponding to the specified quantity and index and the corresponding pixel size. 301 The image is a 3D dataset where the dimensions are z, y, x. 302 If there are additional parameters, more dimensions are added in the order z, y, x, par1, par2, ... 303 The pixel size is a tuple of 3 Metadata.Item in the order z, y, x. 304 """ 305 pt_type = AnalysisResults.PeakType 306 data = None 307 if pt == pt_type.average: 308 peaks = self.list_existing_peak_types(index) 309 match len(peaks): 310 case 0: 311 raise ValueError( 312 "No peaks found for the specified index. Cannot compute average.") 313 case 1: 314 data = np.array(sync(self._get_quantity(qt, peaks[0], index))) 315 case 2: 316 data1, data2 = _gather_sync( 317 self._get_quantity(qt, peaks[0], index), 318 self._get_quantity(qt, peaks[1], index) 319 ) 320 data = (np.abs(data1) + np.abs(data2))/2 321 else: 322 data = np.array(sync(self._get_quantity(qt, pt, index))) 323 if self._sparse: 324 sm = np.array(self._spatial_map) 325 img = data[sm, ...] 326 img[sm<0, ...] = np.nan # set invalid pixels to NaN 327 else: 328 img = data 329 return img, self._spatial_map_px_size 330 def get_quantity_at_pixel(self, coord: tuple, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0): 331 """ 332 Synchronous wrapper for `get_quantity_at_pixel_async` (see doc for `brimfile.analysis_results.AnalysisResults.get_quantity_at_pixel_async`) 333 """ 334 return sync(self.get_quantity_at_pixel_async(coord, qt, pt, index)) 335 async def get_quantity_at_pixel_async(self, coord: tuple, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0): 336 """ 337 Retrieves the specified quantity in the image at coord, based on the peak type and index. 338 339 Args: 340 coord (tuple): A tuple of 3 elements corresponding to the z, y, x coordinate in the image 341 qt (Quantity): The quantity to retrieve the image for (e.g. shift). 342 pt (PeakType, optional): The type of peak to consider (default is PeakType.AntiStokes). 343 index (int, optional): The index of the data to retrieve, if multiple peaks are present (default is 0). 344 345 Returns: 346 The requested quantity, which is a scalar or a multidimensional array (depending on whether there are additional parameters in the current Data group) 347 """ 348 if len(coord) != 3: 349 raise ValueError( 350 "'coord' must have 3 elements corresponding to z, y, x") 351 if self._sparse: 352 i = self._spatial_map[*coord] 353 assert i.size == 1 354 if i<0: 355 return np.nan # invalid pixel 356 i = (int(i), ...) 357 else: 358 i = coord + (...,) 359 360 pt_type = AnalysisResults.PeakType 361 value = None 362 if pt == pt_type.average: 363 value = None 364 peaks = await self.list_existing_peak_types_async(index) 365 match len(peaks): 366 case 0: 367 raise ValueError( 368 "No peaks found for the specified index. Cannot compute average.") 369 case 1: 370 data = await self._get_quantity(qt, peaks[0], index) 371 value = await _async_getitem(data, i) 372 case 2: 373 data_p0, data_p1 = await asyncio.gather( 374 self._get_quantity(qt, peaks[0], index), 375 self._get_quantity(qt, peaks[1], index) 376 ) 377 value1, value2 = await asyncio.gather( 378 _async_getitem(data_p0, i), 379 _async_getitem(data_p1, i) 380 ) 381 value = (np.abs(value1) + np.abs(value2))/2 382 else: 383 data = await self._get_quantity(qt, pt, index) 384 value = await _async_getitem(data, i) 385 return value 386 def get_all_quantities_in_image(self, coor: tuple, index_peak: int = 0) -> dict: 387 """ 388 Retrieve all available quantities at a specific spatial coordinate. 389 390 Args: 391 coor (tuple): A tuple containing the z, y, x coordinates in the image. 392 index_peak (int, optional): The index of the data to retrieve, if multiple peaks are present (default is 0). 393 394 Returns: 395 dict: A dictionary of Metadata.Item in the form `result[quantity.name][peak.name] = Metadata.Item(value, units)`. 396 The dictionary contains all available quantities (e.g., Shift, Width, etc.) for both Stokes and AntiStokes peaks, 397 as well as their average values. 398 """ 399 if len(coor) != 3: 400 raise ValueError("coor must contain 3 values for z, y, x") 401 index = int(self._spatial_map[coor]) if self._sparse else coor 402 return sync(self._get_all_quantities_at_index(index, index_peak)) 403 async def _get_all_quantities_at_index(self, index: int | tuple[int, int, int], index_peak: int = 0) -> dict: 404 """ 405 Retrieve all available quantities for a specific spatial index. 406 Args: 407 index (int) | tuple[int, int, int]: The spatial index to retrieve quantities for, which can be a tuple for non-sparse data. 408 index_peak (int, optional): The index of the data to retrieve, if multiple peaks are present (default is 0). 409 Returns: 410 dict: A dictionary of Metadata.Item in the form `result[quantity.name][peak.name] = bls.Metadata.Item(value, units)` 411 """ 412 async def _get_existing_quantity_at_index_async(self, index: int | tuple[int, int, int], pt: AnalysisResults.PeakType = AnalysisResults.PeakType.AntiStokes): 413 as_cls = AnalysisResults 414 qts_ls = () 415 dts_ls = () 416 417 qts = [qt for qt in as_cls.Quantity] 418 coros = [self._file.open_dataset(concatenate_paths(self._path, as_cls._get_quantity_name(qt, pt, index_peak))) for qt in qts] 419 420 # open the datasets asynchronously, excluding those that do not exist 421 opened_dts = await asyncio.gather(*coros, return_exceptions=True) 422 for i, opened_qt in enumerate(opened_dts): 423 if not isinstance(opened_qt, Exception): 424 qts_ls += (qts[i],) 425 dts_ls += (opened_dts[i],) 426 # get the values at the specified index 427 if isinstance(index, tuple): 428 index += (..., ) 429 else: 430 index = (index, ...) 431 coros_values = [_async_getitem(dt, index) for dt in dts_ls] 432 coros_units = [units.of_object(self._file, dt) for dt in dts_ls] 433 ret_ls = await asyncio.gather(*coros_values, *coros_units) 434 n = len(coros_values) 435 value_ls = [Metadata.Item(ret_ls[i], ret_ls[n+i]) for i in range(n)] 436 return qts_ls, value_ls 437 antiStokes, stokes = await asyncio.gather( 438 _get_existing_quantity_at_index_async(self, index, AnalysisResults.PeakType.AntiStokes), 439 _get_existing_quantity_at_index_async(self, index, AnalysisResults.PeakType.Stokes) 440 ) 441 res = {} 442 # combine the results, including the average 443 for qt in (set(antiStokes[0]) | set(stokes[0])): 444 res[qt.name] = {} 445 pts = () 446 #Stokes 447 if qt in stokes[0]: 448 res[qt.name][AnalysisResults.PeakType.Stokes.name] = stokes[1][stokes[0].index(qt)] 449 pts += (AnalysisResults.PeakType.Stokes,) 450 #AntiStokes 451 if qt in antiStokes[0]: 452 res[qt.name][AnalysisResults.PeakType.AntiStokes.name] = antiStokes[1][antiStokes[0].index(qt)] 453 pts += (AnalysisResults.PeakType.AntiStokes,) 454 #average getting the units of the first peak 455 res[qt.name][AnalysisResults.PeakType.average.name] = Metadata.Item( 456 np.mean([np.abs(res[qt.name][pt.name].value) for pt in pts]), 457 res[qt.name][pts[0].name].units 458 ) 459 if not all(res[qt.name][pt.name].units == res[qt.name][pts[0].name].units for pt in pts): 460 warnings.warn(f"The units of {pts} are not consistent.") 461 return res 462 463 @classmethod 464 def _get_quantity_name(cls, qt: Quantity, pt: PeakType, index: int) -> str: 465 """ 466 Returns the name of the dataset correponding to the specific Quantity, PeakType and index 467 468 Args: 469 qt (Quantity) 470 pt (PeakType) 471 intex (int): in case of multiple peaks fitted, the index of the peak to consider 472 """ 473 if not pt in (cls.PeakType.AntiStokes, cls.PeakType.Stokes): 474 raise ValueError("pt has to be either Stokes or AntiStokes") 475 if qt == cls.Quantity.R2 or qt == cls.Quantity.RMSE or qt == cls.Quantity.Cov_matrix: 476 name = f"Fit_error_{str(pt.value)}_{index}/{str(qt.value)}" 477 else: 478 name = f"{str(qt.value)}_{str(pt.value)}_{index}" 479 return name 480 481 async def _get_quantity(self, qt: Quantity, pt: PeakType = PeakType.AntiStokes, index: int = 0): 482 """ 483 Retrieve a specific quantity dataset from the file. 484 485 Args: 486 qt (Quantity): The type of quantity to retrieve. 487 pt (PeakType, optional): The peak type to consider (default is PeakType.AntiStokes). 488 index (int, optional): The index of the quantity if multiple peaks are available (default is 0). 489 490 Returns: 491 The dataset corresponding to the specified quantity, as stored in the file. 492 493 """ 494 495 dt_name = AnalysisResults._get_quantity_name(qt, pt, index) 496 full_path = concatenate_paths(self._path, dt_name) 497 return await self._file.open_dataset(full_path) 498 499 def list_existing_peak_types(self, index: int = 0) -> tuple: 500 """ 501 Synchronous wrapper for `list_existing_peak_types_async` (see doc for `brimfile.analysis_results.AnalysisResults.list_existing_peak_types_async`) 502 """ 503 return sync(self.list_existing_peak_types_async(index)) 504 async def list_existing_peak_types_async(self, index: int = 0) -> tuple: 505 """ 506 Returns a tuple of existing peak types (Stokes and/or AntiStokes) for the specified index. 507 Args: 508 index (int, optional): The index of the peak to check (in case of multi-peak fit). Defaults to 0. 509 Returns: 510 tuple: A tuple containing `PeakType` members (`Stokes`, `AntiStokes`) that exist for the given index. 511 """ 512 513 as_cls = AnalysisResults 514 shift_s_name = as_cls._get_quantity_name( 515 as_cls.Quantity.Shift, as_cls.PeakType.Stokes, index) 516 shift_as_name = as_cls._get_quantity_name( 517 as_cls.Quantity.Shift, as_cls.PeakType.AntiStokes, index) 518 ls = () 519 coro_as_exists = self._file.object_exists(concatenate_paths(self._path, shift_as_name)) 520 coro_s_exists = self._file.object_exists(concatenate_paths(self._path, shift_s_name)) 521 as_exists, s_exists = await asyncio.gather(coro_as_exists, coro_s_exists) 522 if as_exists: 523 ls += (as_cls.PeakType.AntiStokes,) 524 if s_exists: 525 ls += (as_cls.PeakType.Stokes,) 526 return ls 527 528 def list_existing_quantities(self, pt: PeakType = PeakType.AntiStokes, index: int = 0) -> tuple: 529 """ 530 Synchronous wrapper for `list_existing_quantities_async` (see doc for `brimfile.analysis_results.AnalysisResults.list_existing_quantities_async`) 531 """ 532 return sync(self.list_existing_quantities_async(pt, index)) 533 async def list_existing_quantities_async(self, pt: PeakType = PeakType.AntiStokes, index: int = 0) -> tuple: 534 """ 535 Returns a tuple of existing quantities for the specified index. 536 Args: 537 index (int, optional): The index of the peak to check (in case of multi-peak fit). Defaults to 0. 538 Returns: 539 tuple: A tuple containing `Quantity` members that exist for the given index. 540 """ 541 as_cls = AnalysisResults 542 ls = () 543 544 qts = [qt for qt in as_cls.Quantity] 545 coros = [self._file.object_exists(concatenate_paths(self._path, as_cls._get_quantity_name(qt, pt, index))) for qt in qts] 546 547 qt_exists = await asyncio.gather(*coros) 548 for i, exists in enumerate(qt_exists): 549 if exists: 550 ls += (qts[i],) 551 return ls
Rapresents the analysis results associated with a Data object.
Inherited Members
- brimfile.analysis_results.AnalysisResults
- AnalysisResults
- Quantity
- PeakType
- FitModel
- get_name
- add_data
- get_units
- fit_model
- save_image_to_OMETiff
- get_image
- get_quantity_at_pixel
- get_quantity_at_pixel_async
- get_all_quantities_in_image
- list_existing_peak_types
- list_existing_peak_types_async
- list_existing_quantities
- list_existing_quantities_async