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