Source code for ClearMap.IO.Source

# -*- coding: utf-8 -*-
"""
Source
======

This module provides the base class for data sources and sinks.
"""
__author__    = 'Christoph Kirst <christoph.kirst.ck@gmail.com>'
__license__   = 'GPLv3 - GNU General Public License v3 (see LICENSE.txt)'
__copyright__ = 'Copyright © 2020 by Christoph Kirst'
__webpage__   = 'https://idisco.info'
__download__  = 'https://www.github.com/ChristophKirst/ClearMap2'

import warnings
from typing import ClassVar

import numpy as np

import ClearMap.IO.FileUtils as fu

from ClearMap.Utils.Formatting import ensure
from ClearMap.Utils.exceptions import ClearMapValueError, ClearMapRuntimeError


###############################################################################
### Source base class
###############################################################################

[docs] class Source: """Base abstract source class.""" _name: ClassVar[str | None] = None # override in subclasses as class variable # __slots__ = () def __init__(self, name=None, mode=None): """Initialization.""" if name is not None: self._name = name self._mode = mode @property def name(self): """The name of this source. Returns ------- name : str Name of this source. """ mod_name = type(self).__module__.split(".")[-1] name_fallback = f'{mod_name}-Source' cls_name = getattr(self, '_name', name_fallback) if cls_name is None: cls_name = name_fallback return cls_name @name.setter def name(self, value: str): warnings.warn('Setting name on a Source instance is discouraged (reserved for testing/debugging). ' 'Define _name as a class variable in subclasses instead.', UserWarning, stacklevel=2) self._name = ensure(value, str) @property def shape(self): """The shape of the source. Returns ------- shape : tuple The shape of the source. """ return None @shape.setter def shape(self, value): raise ValueError('Cannot set shape for this source.') @property def dtype(self): """The data type of the source. Returns ------- dtype : dtype The data type of the source. """ return None @dtype.setter def dtype(self, value): raise ClearMapValueError('Cannot set dtype for this source.') # FIXME: Move to NotImplementedError @property def order(self): """The contiguous order of the underlying data array. Returns ------- order : str Returns 'C' for C contiguous and 'F' for Fortran contiguous, None otherwise. """ return None @order.setter def order(self, value): raise ValueError('Cannot set order for this source.') @property def location(self): """The location where the data of the source is stored. Returns ------- location : str or None Returns the location of the data source or None if this source lives in memory only. """ return None @location.setter def location(self, value): raise ValueError('Cannot set location for this source.') ### Derived properties @property def ndim(self): """The number of dimensions of the source. Returns ------- ndim : int The number of dimension of the source. """ return len(self.shape) @property def size(self): """The size of the source. Returns ------- size : int The number of data items in the source. """ return np.prod(self.shape) ### Functionality
[docs] def exists(self): if self.location is not None: return fu.is_file(self.location) else: return False
### Source conversions
[docs] def as_virtual(self): """Return virtual source without array data to pass in parallel processing. Returns ------- source : Source class The source class without array data. """ # return VirtualSource(source = self.source) raise NotImplementedError('virtual source not implemented for this source!')
[docs] def as_real(self): return self
[docs] def as_buffer(self): raise NotImplementedError('buffer not implemented for this source!')
[docs] def as_memory(self): return np.array(self.as_buffer())
### Data @property def mode(self): return self._mode @property def is_writable(self): """True if in-memory writes are permitted (mode != 'r').""" return self._mode != 'r' @property def is_persistable(self): """True if changes can be written back to disk (mode 'r+' or 'w+').""" return self._mode in ('r+', 'w+', None) def __getitem__(self, *args): raise KeyError('No getitem routine for this source!') def __setitem__(self, *args): if not self.is_writable: raise PermissionError( f'Source {self} was opened read-only (mode="r"). ' f'Use io.edit() to open for writing.') raise KeyError('No setitem routine for this source!')
[docs] def read(self, *args, **kwargs): raise KeyError('No read routine for this source!')
[docs] def write(self, *args, **kwargs): raise KeyError('No write routine for this source!')
### Formatting def __str__(self): try: name = self.name name = f'{name}' if name is not None else '' except: # print('name') name ='' try: shape = self.shape shape ='%r' % ((shape,)) if shape is not None else '' except: # print('shape') shape = '' try: dtype = self.dtype dtype = f'[{dtype}]' if dtype is not None else '' except: # print('dtype') dtype = '' try: order = self.order order = f'|{order}|' if order is not None else '' except: # print('order') order = '' # try: # memory = self.memory # memory = '<%s>' % memory if memory is not None else '' # except: # memory = '' try: location = self.location location = '%s' % location if location is not None else '' if len(location) > 100: location = location[:50] + '...' + location[-50:] if len(location) > 0: location = '{%s}' % location except: # print('location') location = '' # try: # array = self.array.__str__() # if len(array) > 100: # e = array[100:].find('\n') # if e != -1: # array = array[:100 + e] + '...' # if len(array) > 0: # array = '\n' + array # except: # array = '' return name + shape + dtype + order + location # + array def __repr__(self): return self.__str__()
############################################################################### ### Abstract and VirtualSource base class ############################################################################### # TODO: memory -> device argument
[docs] class AbstractSource(Source): """Abstract source to handle data sources without data in memory. Note ---- This class handles essential info about a source and to how access its data. """ # __slots__ = ('_shape', '_dtype', '_order', '_location') def __init__(self, source=None, shape=None, dtype=None, order=None, location =None, name=None, mode=None): """Source class constructor. Arguments --------- source : shape : tuple of int or None Shape of the source, if None try to determine from source. dtype : dtype or None The data type of the source, if None try to determine from source. order : 'C' or 'F' or None The order of the source, c or Fortran contiguous. location : str or None The location of the source. name : str | None mode : str | None """ super().__init__(name=name, mode=mode) if source is not None: if shape is None and hasattr(source, 'shape'): shape = source.shape if dtype is None and hasattr(source, 'dtype'): dtype = source.dtype if order is None and hasattr(source, 'order'): order = source.order # if memory is None and hasattr(source, 'memory'): # memory = memory.order if location is None and hasattr(source, 'location'): location = source.location if hasattr(source, 'mode'): # Don't add to sources that don't have that attr if mode is None: mode = source.mode self._mode = ensure(mode, str) self._shape = ensure(shape, tuple) self._dtype = ensure(dtype, np.dtype) self._order = ensure(order, str) # self._memory = ensure(memory, str) self._location = ensure(location, str) @property def shape(self): """The shape of the source. Returns ------- shape : tuple The shape of the source. """ return self._shape @shape.setter def shape(self, value): self._shape = ensure(value, tuple) @property def dtype(self): """The data type of the source. Returns ------- dtype : dtype The data type of the source. """ return self._dtype @dtype.setter def dtype(self, value): self._dtype = ensure(value, np.dtype) @property def order(self): """The contiguous order of the data array of the source. Returns ------- order : str Returns 'C' for C and 'F' for fortran contiguous arrays, None otherwise. """ return self._order @order.setter def order(self, value): if value not in [None, 'C', 'F']: raise ValueError("Order %r not in [None, 'C' or 'F']!" % value) self._order = ensure(value, str) @property def location(self): """The location of the source's data. Returns ------- location : str or None Returns the location of the data source or None if there is none. """ return self._location @location.setter def location(self, value): self._location = ensure(value, str)
[docs] def as_virtual(self): return self
[docs] def as_real(self): raise ClearMapRuntimeError('The abstract source cannot be converted to a real source!')
[docs] def as_buffer(self): raise ClearMapRuntimeError('The abstract source cannot be converted to a buffer!')
[docs] class VirtualSource(AbstractSource): """Virtual source to handle data sources without data in memory. Note ---- This class is fast to serialize and useful as a source pointer in parallel processing. """ _real_class: ClassVar[type['Source'] | None] = None # Subclasses set this to their concrete Source class def __init__(self, source=None, shape=None, dtype=None, order=None, location=None, name=None, mode=None): AbstractSource.__init__(self, source=source, shape=shape, dtype=dtype, order=order, location=location, name=name, mode=mode) @property def name(self): mod_name = type(self).__module__.split(".")[-1] return getattr(self, '_name', f'Virtual-{mod_name}-Source') def __getitem__(self, *args): return self.as_real().__getitem__(*args) def __setitem__(self, *args): self.as_real().__setitem__(*args)
[docs] def read(self, *args, **kwargs): return self.as_real().read(*args, **kwargs)
[docs] def write(self, *args, **kwargs): self.as_real().write(*args, **kwargs)
[docs] def as_virtual(self): return self # always true for VirtualSource — move to parent
[docs] def as_buffer(self): return self.as_real().as_buffer()
[docs] def as_real(self): """Default: reopen from location with mode. Override in modules that need extra constructor args.""" if self._real_class is None: raise ClearMapRuntimeError(f'{self.__class__.__name__} must set _real_class or override as_real()') return self._real_class(location=self.location, mode=self._mode)
############################################################################### ### Tests ############################################################################### def _test(): import ClearMap.IO.Source as src # reload(src) s = src.VirtualSource(shape=(50,50), dtype=float, location='/tmp/test.npy', order='F') print(s) print(s.size, s.ndim)