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