brimfile.file v1.7.0rc1

  1from __future__ import annotations
  2
  3import numpy as np
  4import warnings
  5
  6from .data import Data
  7
  8from .utils import concatenate_paths
  9from .constants import brim_obj_names
 10from . import units
 11from . import subtypes
 12
 13from .file_abstraction import FileAbstraction, StoreType, sync, Version
 14from .validation import validate_json, ValidationError, ValidationLevel
 15from .validation.json_descriptor import generate_json_descriptor
 16
 17# don't import _AbstractFile if running in pyodide (it is defined in js)
 18import sys
 19if "pyodide" not in sys.modules:
 20    from .file_abstraction import _AbstractFile
 21
 22class File:
 23    """
 24    High-level entry point for reading and writing brim files.
 25
 26    Use this class to open an existing brim file (`File(...)`) or create a new
 27    one (`File.create(...)`), then access data groups, metadata, analysis
 28    results, and validation helpers.
 29    """
 30
 31    if "pyodide" in sys.modules:
 32        def __init__(self, file):
 33            self._file = file
 34            self._file.version = self._get_file_version_tuple()
 35            if not self.is_valid():
 36                raise ValueError("The brim file is not valid!")
 37    else:
 38        def __init__(self, filename: str, mode: str = 'r',
 39                     store_type: StoreType = StoreType.AUTO, * ,
 40                     validate: bool = False) -> None:
 41            """
 42            Initialize the File object.
 43
 44            Args:
 45                filename (str): Path to the brim file.
 46                mode: {'r', 'r+', 'a', 'w', 'w-'} the mode for opening the file (default is 'r' for read-only).
 47                            See the definition of `mode` in `brimfile.file_abstraction._zarrFile.__init__()` for more details.
 48                            'r' means read only (must exist); 'r+' means read/write (must exist);
 49                            'a' means read/write (create if doesn't exist); 'w' means create (overwrite if exists); 'w-' means create (fail if exists).
 50                store_type (StoreType): Type of the store to use, as defined in `brimfile.file_abstraction.StoreType`. Default is 'AUTO'.
 51                validate (bool): Whether to validate the file upon initialization. Default is False.
 52            """
 53            self._file = _AbstractFile(
 54                filename, mode=mode, store_type=store_type)
 55            self._file.version = self._get_file_version_tuple()
 56            if not self.is_valid():
 57                raise ValueError("The brim file is not valid!")
 58            if validate:
 59                validation_errors: list[ValidationError] = self.validate()
 60                for err in validation_errors:
 61                    if err.level == ValidationLevel.WARNING or err.level == ValidationLevel.ERROR:
 62                        warnings.warn(f"Validation warning at {err.path}: {err.message}")
 63                    elif err.level == ValidationLevel.CRITICAL:
 64                        raise ValueError(f"Validation error at {err.path}: {err.message}")
 65
 66    @staticmethod
 67    def _parse_version_tuple3(version: str) -> Version:
 68        """Normalize a version value into a 3-item tuple (major, minor, patch)."""
 69        if isinstance(version, str):
 70            parts = [part for part in version.split('.') if part != '']
 71        elif isinstance(version, (list, tuple)):
 72            parts = list(version)
 73        else:
 74            raise ValueError(f"Invalid version format: {version}")
 75
 76        # convert the first 3 parts to int if they are digits (str), otherwise keep them as they are
 77        normalized: list[int | str] = []
 78        for part in parts[:3]:
 79            if isinstance(part, str) and part.isdigit():
 80                normalized.append(int(part))
 81            else:
 82                try:
 83                    part = int(part)
 84                    normalized.append(part)
 85                except (ValueError, TypeError):
 86                    raise ValueError(f"Invalid version part: {part}")                
 87
 88        # add zeros to the end of the list until it has 3 elements
 89        while len(normalized) < 3:
 90            normalized.append(0)
 91
 92        return (normalized[0], normalized[1], normalized[2])
 93
 94    def _get_file_version_tuple(self) -> Version:
 95        """
 96        Read brim_version from file root attributes and return a 3-item tuple.
 97        Returns:
 98            Version: A tuple representing the version of the brim file in the format (major, minor, patch).
 99            If the version can't be read from the file, returns None.
100        """
101        try:
102            version = sync(self._file.get_attr('/', 'brim_version'))
103        except Exception:
104            return None
105        return self._parse_version_tuple3(version)
106
107    def validate(self) -> list[ValidationError]:
108        """
109        Validate the brim file and return a list of validation errors.
110
111        Returns:
112            list[ValidationError]: A list of validation errors found in the brim file.
113            If the list is empty, the file is valid.
114        """
115        json_descriptor = generate_json_descriptor(self._file)
116        validation_errors: list[ValidationError] = validate_json(json_descriptor)
117        return validation_errors
118    
119    def __del__(self):
120        try:
121            if hasattr(self, '_file'):
122                self.close()
123        except Exception as e:            
124            # don't throw an error if the file cannot be closed
125            warnings.warn(f"Cannot close the file: {e}")
126
127    def close(self) -> None:
128        """Close the underlying store and release file resources."""
129        self._file.close()
130
131    def is_read_only(self) -> bool:
132        """
133        Return whether the file was opened in read-only mode.
134
135        Returns:
136            bool: ``True`` when write operations are not allowed.
137        """
138        return sync(self._file.is_read_only())
139
140    def is_valid(self) -> bool:
141        """
142        Check if the file is a valid brim file.
143
144        Returns:
145            bool: True if the file is valid, False otherwise.
146        """
147        # TODO validate file against https://github.com/brillouin-imaging/Brillouin-standard-file/blob/main/docs/brim_file_specs.md
148        return True
149
150    @classmethod
151    def create(cls, filename: str, store_type: StoreType = StoreType.AUTO, *, 
152               brim_version: str = '0.2') -> 'File':
153        """
154        Create a new brim file with the specified filename. If the file exists already it will generate an error.
155
156        Args:
157            filename (str): Path to the brim file to be created.
158            store_type (StoreType): Type of the store to use, as defined in `brimfile.file_abstraction.StoreType`. Default is 'AUTO'.
159            brim_version (str): Version of the brim file format to use. Default is '0.2'.
160
161        Returns:
162            File: An instance of the File class representing the newly created brim file.
163            store_type (str): Type of the store to use, as defined in `brimfile.file_abstraction.StoreType`. Default is 'AUTO'.
164        """
165        f = cls(filename, mode='w-', store_type=store_type)
166
167        # File version
168        f._file.version = cls._parse_version_tuple3(brim_version)
169        sync(f._file.create_attr('/', 'brim_version', brim_version))
170
171        # Root Brillouin_data group
172        sync(f._file.create_group(brim_obj_names.Brillouin_base_path))
173
174        return f
175
176    def create_data_group(self, PSD: np.ndarray, frequency: np.ndarray, px_size_um: tuple, *, index: int = None,
177                          name: str = None, compression: FileAbstraction.Compression = FileAbstraction.Compression()) -> 'Data':
178        """
179        Adds a new data entry to the file.
180        Parameters:
181            PSD (np.ndarray): The Power Spectral Density (PSD) data to be added. It must be 4D with dimensions z, y, x, spectrum
182            frequency (np.ndarray): The frequency data corresponding to the PSD. It must be broadcastable to the PSD shape (the most common case is frequency being 1D, in which case the frequency axis is assumed the same for all the spatial coordinates)
183            px_size_um (tuple): A tuple of 3 elements, in the order z,y,x, corresponding to the pixel size in um. Unused dimensions can be set to None.
184            index (int, optional): The index for the new data group. If None, the next available index is used. Defaults to None.
185            name (str, optional): The name for the new data group. Defaults to None.
186            compression (FileAbstraction.Compression, optional): The compression method to use for the data. Defaults to FileAbstraction.Compression.DEFAULT.
187        Returns:
188            Data: The newly created Data object.
189        Raises:
190            IndexError: If the specified index already exists in the dataset.
191            ValueError: If any of the data provided is not valid or consistent
192        """
193        if PSD.ndim != 4:
194            raise ValueError(
195                "'PSD' must have 4 dimensions (z, y, x, spectrum)")
196        try:
197            np.broadcast_shapes(tuple(frequency.shape), tuple(PSD.shape))
198        except ValueError as e:
199            raise ValueError(f"frequency (shape: {frequency.shape}) is not broadcastable to PSD (shape: {PSD.shape}): {e}")
200        if len(px_size_um) != 3:
201            raise ValueError("'px_size_um' must have 3 elements (z,y,x); unused dimensions can be set to None")
202
203        return self._create_data_group_raw(PSD, frequency, scanning = None, sparse = False, px_size_um=px_size_um, 
204                                             index=index, name=name, compression=compression)
205
206    def create_data_group_sparse(self, PSD: np.ndarray, frequency: np.ndarray, scanning: dict, *,
207                                index: int = None, name: str = None, compression: FileAbstraction.Compression = FileAbstraction.Compression()) -> 'Data':
208        """
209        Adds a new [sparse data entry](https://github.com/brillouin-imaging/Brillouin-standard-file/blob/main/docs/brim_file_specs.md) to the file.
210        
211        Sparse data allows storage of spectra in a flattened format (first dimension is the spectrum index),
212        with spatial mapping provided separately. This is efficient for data with irregular sampling or missing pixels.
213        
214        Parameters:
215            PSD (np.ndarray): The Power Spectral Density (PSD) data to be added. First dimension is spectrum index,
216                last dimension contains the spectral data. Shape: (n_spectra, ..., n_freq_points).
217            frequency (np.ndarray): The frequency data corresponding to the PSD. Must be broadcastable to the PSD array.
218            scanning (dict): Dictionary defining the spatial mapping. Must include at least 'Spatial_map' or 'Cartesian_visualisation'.
219                See `brimfile.data.Data._add_data` docstring for detailed structure of the scanning dictionary.
220            index (int, optional): The index for the new data group. If None, the next available index is used. Defaults to None.
221            name (str, optional): The name for the new data group. Defaults to None.
222            compression (FileAbstraction.Compression, optional): The compression method to use for the data. Defaults to FileAbstraction.Compression.DEFAULT.
223        Returns:
224            Data: The newly created Data object.
225        Raises:
226            IndexError: If the specified index already exists in the dataset.
227            ValueError: If any of the data provided is not valid or consistent
228        """
229        return self._create_data_group_raw(PSD, frequency, scanning=scanning, sparse=True, index=index, name=name, compression=compression)   
230    
231    def _create_data_group_raw(self, PSD: np.ndarray, frequency: np.ndarray, *, scanning: dict = None, px_size_um = None, sparse: bool = False,
232                                index: int = None, name: str = None, compression: FileAbstraction.Compression = FileAbstraction.Compression()) -> 'Data':
233        """
234        Adds a new data entry to the file. Check the documentation for `brimfile.data.Data._add_data` for more details on the parameters.
235        Parameters:
236            PSD (np.ndarray): The Power Spectral Density (PSD) data to be added. The last dimension contains the spectra.
237            frequency (np.ndarray): The frequency data corresponding to the PSD. Must be broadcastable to the PSD array.
238            scanning (dict, optional): Spatial mapping metadata. Required for sparse=True, optional for sparse=False.
239                For sparse data, include either `Spatial_map` or `Cartesian_visualisation`.
240                See `brimfile.data.Data._add_data` for the full accepted structure.
241            px_size_um (tuple, optional): A tuple of 3 elements (z, y, x) for pixel size in μm. For non-sparse data only.
242            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.
243            index (int, optional): The index for the new data group. If None, the next available index is used. Defaults to None.
244            name (str, optional): The name for the new data group. Defaults to None.
245            compression (FileAbstraction.Compression, optional): The compression method to use for the data. Defaults to FileAbstraction.Compression.DEFAULT.
246        Returns:
247            Data: The newly created Data object.
248        Raises:
249            IndexError: If the specified index already exists in the dataset.
250            ValueError: If any of the data provided is not valid or consistent
251        """
252        if index is not None:
253            if sync(Data._get_existing_group_name_async(self._file, index)) is not None:
254                raise IndexError(
255                    f"Data {index} already exists in {self._file.filename}")
256        else:
257            data_groups = self.list_data_groups()
258            indices = [dg['index'] for dg in data_groups]
259            indices.sort()
260            index = indices[-1] + 1 if indices else 0  # Next available index
261
262        # create the data group
263        d = Data._create_new(self._file, index, sparse, name)
264        # add the pixel size as an attribute of the data group
265        if px_size_um is not None:
266            sync(self._file.create_attr(d._group, 'element_size', tuple(px_size_um)))
267            units.add_to_attribute(self._file, d._group, 'element_size', 'um')
268        elif not sparse:
269            warnings.warn("Pixel size is not provided for non-sparse data. It is recommended to provide it for proper spatial calibration and visualization.")
270        # add the data to the data group
271        d._add_data(PSD, frequency, scanning = scanning, compression=compression)
272        return d
273
274    def list_data_groups(self, retrieve_custom_name=False) -> list:
275        """
276        List all data groups in the brim file.
277
278        Returns:
279            See documentation of brimfile.data.Data.list_data_groups
280        """
281        return Data.list_data_groups(self._file, retrieve_custom_name)
282
283    def get_data(self, index: int = 0) -> 'Data':
284        """
285        Retrieve a Data object for the specified index.
286
287        Args:
288            index (int): The index of the data group to retrieve.
289
290        Returns:
291            Data: The Data object corresponding to the specified index.
292        Raises:
293            IndexError: If the specified index does not exist in the dataset.
294        """        
295        return sync(Data.from_existing_async(self._file, index))
296
297    @property
298    def filename(self) -> str:
299        """
300        Get the filename of the brim file.
301
302        Returns:
303            str: The filename of the brim file.
304        """
305        return self._file.filename
306    
307    @property
308    def subtype(self) -> subtypes.SubType:
309        """
310        Get the subtype of the brim file.
311
312        Returns:
313            subtypes.SubType: The subtype of the brim file.
314        """
315        return subtypes.get_subtype(self._file)
class File:
 23class File:
 24    """
 25    High-level entry point for reading and writing brim files.
 26
 27    Use this class to open an existing brim file (`File(...)`) or create a new
 28    one (`File.create(...)`), then access data groups, metadata, analysis
 29    results, and validation helpers.
 30    """
 31
 32    if "pyodide" in sys.modules:
 33        def __init__(self, file):
 34            self._file = file
 35            self._file.version = self._get_file_version_tuple()
 36            if not self.is_valid():
 37                raise ValueError("The brim file is not valid!")
 38    else:
 39        def __init__(self, filename: str, mode: str = 'r',
 40                     store_type: StoreType = StoreType.AUTO, * ,
 41                     validate: bool = False) -> None:
 42            """
 43            Initialize the File object.
 44
 45            Args:
 46                filename (str): Path to the brim file.
 47                mode: {'r', 'r+', 'a', 'w', 'w-'} the mode for opening the file (default is 'r' for read-only).
 48                            See the definition of `mode` in `brimfile.file_abstraction._zarrFile.__init__()` for more details.
 49                            'r' means read only (must exist); 'r+' means read/write (must exist);
 50                            'a' means read/write (create if doesn't exist); 'w' means create (overwrite if exists); 'w-' means create (fail if exists).
 51                store_type (StoreType): Type of the store to use, as defined in `brimfile.file_abstraction.StoreType`. Default is 'AUTO'.
 52                validate (bool): Whether to validate the file upon initialization. Default is False.
 53            """
 54            self._file = _AbstractFile(
 55                filename, mode=mode, store_type=store_type)
 56            self._file.version = self._get_file_version_tuple()
 57            if not self.is_valid():
 58                raise ValueError("The brim file is not valid!")
 59            if validate:
 60                validation_errors: list[ValidationError] = self.validate()
 61                for err in validation_errors:
 62                    if err.level == ValidationLevel.WARNING or err.level == ValidationLevel.ERROR:
 63                        warnings.warn(f"Validation warning at {err.path}: {err.message}")
 64                    elif err.level == ValidationLevel.CRITICAL:
 65                        raise ValueError(f"Validation error at {err.path}: {err.message}")
 66
 67    @staticmethod
 68    def _parse_version_tuple3(version: str) -> Version:
 69        """Normalize a version value into a 3-item tuple (major, minor, patch)."""
 70        if isinstance(version, str):
 71            parts = [part for part in version.split('.') if part != '']
 72        elif isinstance(version, (list, tuple)):
 73            parts = list(version)
 74        else:
 75            raise ValueError(f"Invalid version format: {version}")
 76
 77        # convert the first 3 parts to int if they are digits (str), otherwise keep them as they are
 78        normalized: list[int | str] = []
 79        for part in parts[:3]:
 80            if isinstance(part, str) and part.isdigit():
 81                normalized.append(int(part))
 82            else:
 83                try:
 84                    part = int(part)
 85                    normalized.append(part)
 86                except (ValueError, TypeError):
 87                    raise ValueError(f"Invalid version part: {part}")                
 88
 89        # add zeros to the end of the list until it has 3 elements
 90        while len(normalized) < 3:
 91            normalized.append(0)
 92
 93        return (normalized[0], normalized[1], normalized[2])
 94
 95    def _get_file_version_tuple(self) -> Version:
 96        """
 97        Read brim_version from file root attributes and return a 3-item tuple.
 98        Returns:
 99            Version: A tuple representing the version of the brim file in the format (major, minor, patch).
100            If the version can't be read from the file, returns None.
101        """
102        try:
103            version = sync(self._file.get_attr('/', 'brim_version'))
104        except Exception:
105            return None
106        return self._parse_version_tuple3(version)
107
108    def validate(self) -> list[ValidationError]:
109        """
110        Validate the brim file and return a list of validation errors.
111
112        Returns:
113            list[ValidationError]: A list of validation errors found in the brim file.
114            If the list is empty, the file is valid.
115        """
116        json_descriptor = generate_json_descriptor(self._file)
117        validation_errors: list[ValidationError] = validate_json(json_descriptor)
118        return validation_errors
119    
120    def __del__(self):
121        try:
122            if hasattr(self, '_file'):
123                self.close()
124        except Exception as e:            
125            # don't throw an error if the file cannot be closed
126            warnings.warn(f"Cannot close the file: {e}")
127
128    def close(self) -> None:
129        """Close the underlying store and release file resources."""
130        self._file.close()
131
132    def is_read_only(self) -> bool:
133        """
134        Return whether the file was opened in read-only mode.
135
136        Returns:
137            bool: ``True`` when write operations are not allowed.
138        """
139        return sync(self._file.is_read_only())
140
141    def is_valid(self) -> bool:
142        """
143        Check if the file is a valid brim file.
144
145        Returns:
146            bool: True if the file is valid, False otherwise.
147        """
148        # TODO validate file against https://github.com/brillouin-imaging/Brillouin-standard-file/blob/main/docs/brim_file_specs.md
149        return True
150
151    @classmethod
152    def create(cls, filename: str, store_type: StoreType = StoreType.AUTO, *, 
153               brim_version: str = '0.2') -> 'File':
154        """
155        Create a new brim file with the specified filename. If the file exists already it will generate an error.
156
157        Args:
158            filename (str): Path to the brim file to be created.
159            store_type (StoreType): Type of the store to use, as defined in `brimfile.file_abstraction.StoreType`. Default is 'AUTO'.
160            brim_version (str): Version of the brim file format to use. Default is '0.2'.
161
162        Returns:
163            File: An instance of the File class representing the newly created brim file.
164            store_type (str): Type of the store to use, as defined in `brimfile.file_abstraction.StoreType`. Default is 'AUTO'.
165        """
166        f = cls(filename, mode='w-', store_type=store_type)
167
168        # File version
169        f._file.version = cls._parse_version_tuple3(brim_version)
170        sync(f._file.create_attr('/', 'brim_version', brim_version))
171
172        # Root Brillouin_data group
173        sync(f._file.create_group(brim_obj_names.Brillouin_base_path))
174
175        return f
176
177    def create_data_group(self, PSD: np.ndarray, frequency: np.ndarray, px_size_um: tuple, *, index: int = None,
178                          name: str = None, compression: FileAbstraction.Compression = FileAbstraction.Compression()) -> 'Data':
179        """
180        Adds a new data entry to the file.
181        Parameters:
182            PSD (np.ndarray): The Power Spectral Density (PSD) data to be added. It must be 4D with dimensions z, y, x, spectrum
183            frequency (np.ndarray): The frequency data corresponding to the PSD. It must be broadcastable to the PSD shape (the most common case is frequency being 1D, in which case the frequency axis is assumed the same for all the spatial coordinates)
184            px_size_um (tuple): A tuple of 3 elements, in the order z,y,x, corresponding to the pixel size in um. Unused dimensions can be set to None.
185            index (int, optional): The index for the new data group. If None, the next available index is used. Defaults to None.
186            name (str, optional): The name for the new data group. Defaults to None.
187            compression (FileAbstraction.Compression, optional): The compression method to use for the data. Defaults to FileAbstraction.Compression.DEFAULT.
188        Returns:
189            Data: The newly created Data object.
190        Raises:
191            IndexError: If the specified index already exists in the dataset.
192            ValueError: If any of the data provided is not valid or consistent
193        """
194        if PSD.ndim != 4:
195            raise ValueError(
196                "'PSD' must have 4 dimensions (z, y, x, spectrum)")
197        try:
198            np.broadcast_shapes(tuple(frequency.shape), tuple(PSD.shape))
199        except ValueError as e:
200            raise ValueError(f"frequency (shape: {frequency.shape}) is not broadcastable to PSD (shape: {PSD.shape}): {e}")
201        if len(px_size_um) != 3:
202            raise ValueError("'px_size_um' must have 3 elements (z,y,x); unused dimensions can be set to None")
203
204        return self._create_data_group_raw(PSD, frequency, scanning = None, sparse = False, px_size_um=px_size_um, 
205                                             index=index, name=name, compression=compression)
206
207    def create_data_group_sparse(self, PSD: np.ndarray, frequency: np.ndarray, scanning: dict, *,
208                                index: int = None, name: str = None, compression: FileAbstraction.Compression = FileAbstraction.Compression()) -> 'Data':
209        """
210        Adds a new [sparse data entry](https://github.com/brillouin-imaging/Brillouin-standard-file/blob/main/docs/brim_file_specs.md) to the file.
211        
212        Sparse data allows storage of spectra in a flattened format (first dimension is the spectrum index),
213        with spatial mapping provided separately. This is efficient for data with irregular sampling or missing pixels.
214        
215        Parameters:
216            PSD (np.ndarray): The Power Spectral Density (PSD) data to be added. First dimension is spectrum index,
217                last dimension contains the spectral data. Shape: (n_spectra, ..., n_freq_points).
218            frequency (np.ndarray): The frequency data corresponding to the PSD. Must be broadcastable to the PSD array.
219            scanning (dict): Dictionary defining the spatial mapping. Must include at least 'Spatial_map' or 'Cartesian_visualisation'.
220                See `brimfile.data.Data._add_data` docstring for detailed structure of the scanning dictionary.
221            index (int, optional): The index for the new data group. If None, the next available index is used. Defaults to None.
222            name (str, optional): The name for the new data group. Defaults to None.
223            compression (FileAbstraction.Compression, optional): The compression method to use for the data. Defaults to FileAbstraction.Compression.DEFAULT.
224        Returns:
225            Data: The newly created Data object.
226        Raises:
227            IndexError: If the specified index already exists in the dataset.
228            ValueError: If any of the data provided is not valid or consistent
229        """
230        return self._create_data_group_raw(PSD, frequency, scanning=scanning, sparse=True, index=index, name=name, compression=compression)   
231    
232    def _create_data_group_raw(self, PSD: np.ndarray, frequency: np.ndarray, *, scanning: dict = None, px_size_um = None, sparse: bool = False,
233                                index: int = None, name: str = None, compression: FileAbstraction.Compression = FileAbstraction.Compression()) -> 'Data':
234        """
235        Adds a new data entry to the file. Check the documentation for `brimfile.data.Data._add_data` for more details on the parameters.
236        Parameters:
237            PSD (np.ndarray): The Power Spectral Density (PSD) data to be added. The last dimension contains the spectra.
238            frequency (np.ndarray): The frequency data corresponding to the PSD. Must be broadcastable to the PSD array.
239            scanning (dict, optional): Spatial mapping metadata. Required for sparse=True, optional for sparse=False.
240                For sparse data, include either `Spatial_map` or `Cartesian_visualisation`.
241                See `brimfile.data.Data._add_data` for the full accepted structure.
242            px_size_um (tuple, optional): A tuple of 3 elements (z, y, x) for pixel size in μm. For non-sparse data only.
243            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.
244            index (int, optional): The index for the new data group. If None, the next available index is used. Defaults to None.
245            name (str, optional): The name for the new data group. Defaults to None.
246            compression (FileAbstraction.Compression, optional): The compression method to use for the data. Defaults to FileAbstraction.Compression.DEFAULT.
247        Returns:
248            Data: The newly created Data object.
249        Raises:
250            IndexError: If the specified index already exists in the dataset.
251            ValueError: If any of the data provided is not valid or consistent
252        """
253        if index is not None:
254            if sync(Data._get_existing_group_name_async(self._file, index)) is not None:
255                raise IndexError(
256                    f"Data {index} already exists in {self._file.filename}")
257        else:
258            data_groups = self.list_data_groups()
259            indices = [dg['index'] for dg in data_groups]
260            indices.sort()
261            index = indices[-1] + 1 if indices else 0  # Next available index
262
263        # create the data group
264        d = Data._create_new(self._file, index, sparse, name)
265        # add the pixel size as an attribute of the data group
266        if px_size_um is not None:
267            sync(self._file.create_attr(d._group, 'element_size', tuple(px_size_um)))
268            units.add_to_attribute(self._file, d._group, 'element_size', 'um')
269        elif not sparse:
270            warnings.warn("Pixel size is not provided for non-sparse data. It is recommended to provide it for proper spatial calibration and visualization.")
271        # add the data to the data group
272        d._add_data(PSD, frequency, scanning = scanning, compression=compression)
273        return d
274
275    def list_data_groups(self, retrieve_custom_name=False) -> list:
276        """
277        List all data groups in the brim file.
278
279        Returns:
280            See documentation of brimfile.data.Data.list_data_groups
281        """
282        return Data.list_data_groups(self._file, retrieve_custom_name)
283
284    def get_data(self, index: int = 0) -> 'Data':
285        """
286        Retrieve a Data object for the specified index.
287
288        Args:
289            index (int): The index of the data group to retrieve.
290
291        Returns:
292            Data: The Data object corresponding to the specified index.
293        Raises:
294            IndexError: If the specified index does not exist in the dataset.
295        """        
296        return sync(Data.from_existing_async(self._file, index))
297
298    @property
299    def filename(self) -> str:
300        """
301        Get the filename of the brim file.
302
303        Returns:
304            str: The filename of the brim file.
305        """
306        return self._file.filename
307    
308    @property
309    def subtype(self) -> subtypes.SubType:
310        """
311        Get the subtype of the brim file.
312
313        Returns:
314            subtypes.SubType: The subtype of the brim file.
315        """
316        return subtypes.get_subtype(self._file)

High-level entry point for reading and writing brim files.

Use this class to open an existing brim file (File(...)) or create a new one (File.create(...)), then access data groups, metadata, analysis results, and validation helpers.

File( filename: str, mode: str = 'r', store_type: brimfile.file_abstraction.StoreType = <StoreType.AUTO: 'auto'>, *, validate: bool = False)
39        def __init__(self, filename: str, mode: str = 'r',
40                     store_type: StoreType = StoreType.AUTO, * ,
41                     validate: bool = False) -> None:
42            """
43            Initialize the File object.
44
45            Args:
46                filename (str): Path to the brim file.
47                mode: {'r', 'r+', 'a', 'w', 'w-'} the mode for opening the file (default is 'r' for read-only).
48                            See the definition of `mode` in `brimfile.file_abstraction._zarrFile.__init__()` for more details.
49                            'r' means read only (must exist); 'r+' means read/write (must exist);
50                            'a' means read/write (create if doesn't exist); 'w' means create (overwrite if exists); 'w-' means create (fail if exists).
51                store_type (StoreType): Type of the store to use, as defined in `brimfile.file_abstraction.StoreType`. Default is 'AUTO'.
52                validate (bool): Whether to validate the file upon initialization. Default is False.
53            """
54            self._file = _AbstractFile(
55                filename, mode=mode, store_type=store_type)
56            self._file.version = self._get_file_version_tuple()
57            if not self.is_valid():
58                raise ValueError("The brim file is not valid!")
59            if validate:
60                validation_errors: list[ValidationError] = self.validate()
61                for err in validation_errors:
62                    if err.level == ValidationLevel.WARNING or err.level == ValidationLevel.ERROR:
63                        warnings.warn(f"Validation warning at {err.path}: {err.message}")
64                    elif err.level == ValidationLevel.CRITICAL:
65                        raise ValueError(f"Validation error at {err.path}: {err.message}")

Initialize the File object.

Arguments:
  • filename (str): Path to the brim file.
  • mode: {'r', 'r+', 'a', 'w', 'w-'} the mode for opening the file (default is 'r' for read-only). See the definition of mode in brimfile.file_abstraction._zarrFile.__init__() for more details. 'r' means read only (must exist); 'r+' means read/write (must exist); 'a' means read/write (create if doesn't exist); 'w' means create (overwrite if exists); 'w-' means create (fail if exists).
  • store_type (StoreType): Type of the store to use, as defined in brimfile.file_abstraction.StoreType. Default is 'AUTO'.
  • validate (bool): Whether to validate the file upon initialization. Default is False.
def validate(self) -> list[brimfile.validation.main.ValidationError]:
108    def validate(self) -> list[ValidationError]:
109        """
110        Validate the brim file and return a list of validation errors.
111
112        Returns:
113            list[ValidationError]: A list of validation errors found in the brim file.
114            If the list is empty, the file is valid.
115        """
116        json_descriptor = generate_json_descriptor(self._file)
117        validation_errors: list[ValidationError] = validate_json(json_descriptor)
118        return validation_errors

Validate the brim file and return a list of validation errors.

Returns:

list[ValidationError]: A list of validation errors found in the brim file. If the list is empty, the file is valid.

def close(self) -> None:
128    def close(self) -> None:
129        """Close the underlying store and release file resources."""
130        self._file.close()

Close the underlying store and release file resources.

def is_read_only(self) -> bool:
132    def is_read_only(self) -> bool:
133        """
134        Return whether the file was opened in read-only mode.
135
136        Returns:
137            bool: ``True`` when write operations are not allowed.
138        """
139        return sync(self._file.is_read_only())

Return whether the file was opened in read-only mode.

Returns:

bool: True when write operations are not allowed.

def is_valid(self) -> bool:
141    def is_valid(self) -> bool:
142        """
143        Check if the file is a valid brim file.
144
145        Returns:
146            bool: True if the file is valid, False otherwise.
147        """
148        # TODO validate file against https://github.com/brillouin-imaging/Brillouin-standard-file/blob/main/docs/brim_file_specs.md
149        return True

Check if the file is a valid brim file.

Returns:

bool: True if the file is valid, False otherwise.

@classmethod
def create( cls, filename: str, store_type: brimfile.file_abstraction.StoreType = <StoreType.AUTO: 'auto'>, *, brim_version: str = '0.2') -> File:
151    @classmethod
152    def create(cls, filename: str, store_type: StoreType = StoreType.AUTO, *, 
153               brim_version: str = '0.2') -> 'File':
154        """
155        Create a new brim file with the specified filename. If the file exists already it will generate an error.
156
157        Args:
158            filename (str): Path to the brim file to be created.
159            store_type (StoreType): Type of the store to use, as defined in `brimfile.file_abstraction.StoreType`. Default is 'AUTO'.
160            brim_version (str): Version of the brim file format to use. Default is '0.2'.
161
162        Returns:
163            File: An instance of the File class representing the newly created brim file.
164            store_type (str): Type of the store to use, as defined in `brimfile.file_abstraction.StoreType`. Default is 'AUTO'.
165        """
166        f = cls(filename, mode='w-', store_type=store_type)
167
168        # File version
169        f._file.version = cls._parse_version_tuple3(brim_version)
170        sync(f._file.create_attr('/', 'brim_version', brim_version))
171
172        # Root Brillouin_data group
173        sync(f._file.create_group(brim_obj_names.Brillouin_base_path))
174
175        return f

Create a new brim file with the specified filename. If the file exists already it will generate an error.

Arguments:
  • filename (str): Path to the brim file to be created.
  • store_type (StoreType): Type of the store to use, as defined in brimfile.file_abstraction.StoreType. Default is 'AUTO'.
  • brim_version (str): Version of the brim file format to use. Default is '0.2'.
Returns:

File: An instance of the File class representing the newly created brim file. store_type (str): Type of the store to use, as defined in brimfile.file_abstraction.StoreType. Default is 'AUTO'.

def create_data_group( self, PSD: numpy.ndarray, frequency: numpy.ndarray, px_size_um: tuple, *, index: int = None, name: str = None, compression: brimfile.file_abstraction.FileAbstraction.Compression = <brimfile.file_abstraction.FileAbstraction.Compression object>) -> brimfile.data.Data:
177    def create_data_group(self, PSD: np.ndarray, frequency: np.ndarray, px_size_um: tuple, *, index: int = None,
178                          name: str = None, compression: FileAbstraction.Compression = FileAbstraction.Compression()) -> 'Data':
179        """
180        Adds a new data entry to the file.
181        Parameters:
182            PSD (np.ndarray): The Power Spectral Density (PSD) data to be added. It must be 4D with dimensions z, y, x, spectrum
183            frequency (np.ndarray): The frequency data corresponding to the PSD. It must be broadcastable to the PSD shape (the most common case is frequency being 1D, in which case the frequency axis is assumed the same for all the spatial coordinates)
184            px_size_um (tuple): A tuple of 3 elements, in the order z,y,x, corresponding to the pixel size in um. Unused dimensions can be set to None.
185            index (int, optional): The index for the new data group. If None, the next available index is used. Defaults to None.
186            name (str, optional): The name for the new data group. Defaults to None.
187            compression (FileAbstraction.Compression, optional): The compression method to use for the data. Defaults to FileAbstraction.Compression.DEFAULT.
188        Returns:
189            Data: The newly created Data object.
190        Raises:
191            IndexError: If the specified index already exists in the dataset.
192            ValueError: If any of the data provided is not valid or consistent
193        """
194        if PSD.ndim != 4:
195            raise ValueError(
196                "'PSD' must have 4 dimensions (z, y, x, spectrum)")
197        try:
198            np.broadcast_shapes(tuple(frequency.shape), tuple(PSD.shape))
199        except ValueError as e:
200            raise ValueError(f"frequency (shape: {frequency.shape}) is not broadcastable to PSD (shape: {PSD.shape}): {e}")
201        if len(px_size_um) != 3:
202            raise ValueError("'px_size_um' must have 3 elements (z,y,x); unused dimensions can be set to None")
203
204        return self._create_data_group_raw(PSD, frequency, scanning = None, sparse = False, px_size_um=px_size_um, 
205                                             index=index, name=name, compression=compression)

Adds a new data entry to the file.

Arguments:
  • PSD (np.ndarray): The Power Spectral Density (PSD) data to be added. It must be 4D with dimensions z, y, x, spectrum
  • frequency (np.ndarray): The frequency data corresponding to the PSD. It must be broadcastable to the PSD shape (the most common case is frequency being 1D, in which case the frequency axis is assumed the same for all the spatial coordinates)
  • px_size_um (tuple): A tuple of 3 elements, in the order z,y,x, corresponding to the pixel size in um. Unused dimensions can be set to None.
  • index (int, optional): The index for the new data group. If None, the next available index is used. Defaults to None.
  • name (str, optional): The name for the new data group. Defaults to None.
  • compression (FileAbstraction.Compression, optional): The compression method to use for the data. Defaults to FileAbstraction.Compression.DEFAULT.
Returns:

Data: The newly created Data object.

Raises:
  • IndexError: If the specified index already exists in the dataset.
  • ValueError: If any of the data provided is not valid or consistent
def create_data_group_sparse( self, PSD: numpy.ndarray, frequency: numpy.ndarray, scanning: dict, *, index: int = None, name: str = None, compression: brimfile.file_abstraction.FileAbstraction.Compression = <brimfile.file_abstraction.FileAbstraction.Compression object>) -> brimfile.data.Data:
207    def create_data_group_sparse(self, PSD: np.ndarray, frequency: np.ndarray, scanning: dict, *,
208                                index: int = None, name: str = None, compression: FileAbstraction.Compression = FileAbstraction.Compression()) -> 'Data':
209        """
210        Adds a new [sparse data entry](https://github.com/brillouin-imaging/Brillouin-standard-file/blob/main/docs/brim_file_specs.md) to the file.
211        
212        Sparse data allows storage of spectra in a flattened format (first dimension is the spectrum index),
213        with spatial mapping provided separately. This is efficient for data with irregular sampling or missing pixels.
214        
215        Parameters:
216            PSD (np.ndarray): The Power Spectral Density (PSD) data to be added. First dimension is spectrum index,
217                last dimension contains the spectral data. Shape: (n_spectra, ..., n_freq_points).
218            frequency (np.ndarray): The frequency data corresponding to the PSD. Must be broadcastable to the PSD array.
219            scanning (dict): Dictionary defining the spatial mapping. Must include at least 'Spatial_map' or 'Cartesian_visualisation'.
220                See `brimfile.data.Data._add_data` docstring for detailed structure of the scanning dictionary.
221            index (int, optional): The index for the new data group. If None, the next available index is used. Defaults to None.
222            name (str, optional): The name for the new data group. Defaults to None.
223            compression (FileAbstraction.Compression, optional): The compression method to use for the data. Defaults to FileAbstraction.Compression.DEFAULT.
224        Returns:
225            Data: The newly created Data object.
226        Raises:
227            IndexError: If the specified index already exists in the dataset.
228            ValueError: If any of the data provided is not valid or consistent
229        """
230        return self._create_data_group_raw(PSD, frequency, scanning=scanning, sparse=True, index=index, name=name, compression=compression)   

Adds a new sparse data entry to the file.

Sparse data allows storage of spectra in a flattened format (first dimension is the spectrum index), with spatial mapping provided separately. This is efficient for data with irregular sampling or missing pixels.

Arguments:
  • PSD (np.ndarray): The Power Spectral Density (PSD) data to be added. First dimension is spectrum index, last dimension contains the spectral data. Shape: (n_spectra, ..., n_freq_points).
  • frequency (np.ndarray): The frequency data corresponding to the PSD. Must be broadcastable to the PSD array.
  • scanning (dict): Dictionary defining the spatial mapping. Must include at least 'Spatial_map' or 'Cartesian_visualisation'. See brimfile.data.Data._add_data docstring for detailed structure of the scanning dictionary.
  • index (int, optional): The index for the new data group. If None, the next available index is used. Defaults to None.
  • name (str, optional): The name for the new data group. Defaults to None.
  • compression (FileAbstraction.Compression, optional): The compression method to use for the data. Defaults to FileAbstraction.Compression.DEFAULT.
Returns:

Data: The newly created Data object.

Raises:
  • IndexError: If the specified index already exists in the dataset.
  • ValueError: If any of the data provided is not valid or consistent
def list_data_groups(self, retrieve_custom_name=False) -> list:
275    def list_data_groups(self, retrieve_custom_name=False) -> list:
276        """
277        List all data groups in the brim file.
278
279        Returns:
280            See documentation of brimfile.data.Data.list_data_groups
281        """
282        return Data.list_data_groups(self._file, retrieve_custom_name)

List all data groups in the brim file.

Returns:

See documentation of brimfile.data.Data.list_data_groups

def get_data(self, index: int = 0) -> brimfile.data.Data:
284    def get_data(self, index: int = 0) -> 'Data':
285        """
286        Retrieve a Data object for the specified index.
287
288        Args:
289            index (int): The index of the data group to retrieve.
290
291        Returns:
292            Data: The Data object corresponding to the specified index.
293        Raises:
294            IndexError: If the specified index does not exist in the dataset.
295        """        
296        return sync(Data.from_existing_async(self._file, index))

Retrieve a Data object for the specified index.

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

Data: The Data object corresponding to the specified index.

Raises:
  • IndexError: If the specified index does not exist in the dataset.
filename: str
298    @property
299    def filename(self) -> str:
300        """
301        Get the filename of the brim file.
302
303        Returns:
304            str: The filename of the brim file.
305        """
306        return self._file.filename

Get the filename of the brim file.

Returns:

str: The filename of the brim file.

308    @property
309    def subtype(self) -> subtypes.SubType:
310        """
311        Get the subtype of the brim file.
312
313        Returns:
314            subtypes.SubType: The subtype of the brim file.
315        """
316        return subtypes.get_subtype(self._file)

Get the subtype of the brim file.

Returns:

subtypes.SubType: The subtype of the brim file.