class TimeSamples(SamplesGenerator): """ Container for time data in `*.h5` format. This class loads measured data from h5 files and and provides information about this data. It also serves as an interface where the data can be accessed (e.g. for use in a block chain) via the :meth:`result` generator. """ #: Full name of the .h5 file with data. name = File(filter=['*.h5'], desc="name of data file") #: Basename of the .h5 file with data, is set automatically. basename = Property( depends_on='name', #filter=['*.h5'], desc="basename of data file") #: Calibration data, instance of :class:`~acoular.calib.Calib` class, optional . calib = Trait(Calib, desc="Calibration data") #: Number of channels, is set automatically / read from file. numchannels = CLong(0, desc="number of input channels") #: Number of time data samples, is set automatically / read from file. numsamples = CLong(0, desc="number of samples") #: The time data as array of floats with dimension (numsamples, numchannels). data = Any(transient=True, desc="the actual time data array") #: HDF5 file object h5f = Instance(H5FileBase, transient=True) # Checksum over first data entries of all channels _datachecksum = Property() # internal identifier digest = Property(depends_on=['basename', 'calib.digest', '_datachecksum']) def _get__datachecksum(self): return self.data[0, :].sum() @cached_property def _get_digest(self): return digest(self) @cached_property def _get_basename(self): return path.splitext(path.basename(self.name))[0] @on_trait_change('basename') def load_data(self): """ Open the .h5 file and set attributes. """ if not path.isfile(self.name): # no file there self.numsamples = 0 self.numchannels = 0 self.sample_freq = 0 raise IOError("No such file: %s" % self.name) if self.h5f != None: try: self.h5f.close() except IOError: pass file = _get_h5file_class() self.h5f = file(self.name) self.data = self.h5f.get_data_by_reference('time_data') self.sample_freq = self.h5f.get_node_attribute(self.data, 'sample_freq') (self.numsamples, self.numchannels) = self.data.shape def result(self, num=128): """ Python generator that yields the output block-wise. Parameters ---------- num : integer, defaults to 128 This parameter defines the size of the blocks to be yielded (i.e. the number of samples per block) . Returns ------- Samples in blocks of shape (num, numchannels). The last block may be shorter than num. """ if self.numsamples == 0: raise IOError("no samples available") self._datachecksum # trigger checksum calculation i = 0 if self.calib: if self.calib.num_mics == self.numchannels: cal_factor = self.calib.data[newaxis] else: raise ValueError("calibration data not compatible: %i, %i" % \ (self.calib.num_mics, self.numchannels)) while i < self.numsamples: yield self.data[i:i + num] * cal_factor i += num else: while i < self.numsamples: yield self.data[i:i + num] i += num
class MaskedTimeSamples(TimeSamples): """ Container for time data in `*.h5` format. This class loads measured data from h5 files and provides information about this data. It supports storing information about (in)valid samples and (in)valid channels It also serves as an interface where the data can be accessed (e.g. for use in a block chain) via the :meth:`result` generator. """ #: Index of the first sample to be considered valid. start = CLong(0, desc="start of valid samples") #: Index of the last sample to be considered valid. stop = Trait(None, None, CLong, desc="stop of valid samples") #: Channels that are to be treated as invalid. invalid_channels = List(desc="list of invalid channels") #: Channel mask to serve as an index for all valid channels, is set automatically. channels = Property(depends_on=['invalid_channels', 'numchannels_total'], desc="channel mask") #: Number of channels (including invalid channels), is set automatically. numchannels_total = CLong(0, desc="total number of input channels") #: Number of time data samples (including invalid samples), is set automatically. numsamples_total = CLong(0, desc="total number of samples per channel") #: Number of valid channels, is set automatically. numchannels = Property(depends_on = ['invalid_channels', \ 'numchannels_total'], desc="number of valid input channels") #: Number of valid time data samples, is set automatically. numsamples = Property(depends_on=['start', 'stop', 'numsamples_total'], desc="number of valid samples per channel") # internal identifier digest = Property( depends_on = ['basename', 'start', 'stop', \ 'calib.digest', 'invalid_channels','_datachecksum']) @cached_property def _get_digest(self): return digest(self) @cached_property def _get_basename(self): return path.splitext(path.basename(self.name))[0] @cached_property def _get_channels(self): if len(self.invalid_channels) == 0: return slice(0, None, None) allr = [ i for i in range(self.numchannels_total) if i not in self.invalid_channels ] return array(allr) @cached_property def _get_numchannels(self): if len(self.invalid_channels) == 0: return self.numchannels_total return len(self.channels) @cached_property def _get_numsamples(self): sli = slice(self.start, self.stop).indices(self.numsamples_total) return sli[1] - sli[0] @on_trait_change('basename') def load_data(self): #""" open the .h5 file and set attributes #""" if not path.isfile(self.name): # no file there self.numsamples_total = 0 self.numchannels_total = 0 self.sample_freq = 0 raise IOError("No such file: %s" % self.name) if self.h5f != None: try: self.h5f.close() except IOError: pass file = _get_h5file_class() self.h5f = file(self.name) self.data = self.h5f.get_data_by_reference('time_data') self.sample_freq = self.h5f.get_node_attribute(self.data, 'sample_freq') (self.numsamples_total, self.numchannels_total) = self.data.shape def result(self, num=128): """ Python generator that yields the output block-wise. Parameters ---------- num : integer, defaults to 128 This parameter defines the size of the blocks to be yielded (i.e. the number of samples per block). Returns ------- Samples in blocks of shape (num, numchannels). The last block may be shorter than num. """ sli = slice(self.start, self.stop).indices(self.numsamples_total) i = sli[0] stop = sli[1] cal_factor = 1.0 if i >= stop: raise IOError("no samples available") self._datachecksum # trigger checksum calculation if self.calib: if self.calib.num_mics == self.numchannels_total: cal_factor = self.calib.data[self.channels][newaxis] elif self.calib.num_mics == self.numchannels: cal_factor = self.calib.data[newaxis] else: raise ValueError("calibration data not compatible: %i, %i" % \ (self.calib.num_mics, self.numchannels)) while i < stop: yield self.data[i:min(i + num, stop)][:, self.channels] * cal_factor i += num
class MaskedTimeInOut ( TimeInOut ): """ Signal processing block for channel and sample selection. This class serves as intermediary to define (in)valid channels and samples for any :class:`~acoular.sources.SamplesGenerator` (or derived) object. It gets samples from :attr:`~acoular.tprocess.TimeInOut.source` and generates output via the generator :meth:`result`. """ #: Index of the first sample to be considered valid. start = CLong(0, desc="start of valid samples") #: Index of the last sample to be considered valid. stop = Trait(None, None, CLong, desc="stop of valid samples") #: Channels that are to be treated as invalid. invalid_channels = List( desc="list of invalid channels") #: Channel mask to serve as an index for all valid channels, is set automatically. channels = Property(depends_on = ['invalid_channels', 'source.numchannels'], desc="channel mask") #: Number of channels in input, as given by :attr:`~acoular.tprocess.TimeInOut.source`. numchannels_total = Delegate('source', 'numchannels') #: Number of samples in input, as given by :attr:`~acoular.tprocess.TimeInOut.source`. numsamples_total = Delegate('source', 'numsamples') #: Number of valid channels, is set automatically. numchannels = Property(depends_on = ['invalid_channels', \ 'source.numchannels'], desc="number of valid input channels") #: Number of valid time samples, is set automatically. numsamples = Property(depends_on = ['start', 'stop', 'source.numsamples'], desc="number of valid samples per channel") #: Name of the cache file without extension, readonly. basename = Property( depends_on = 'source.digest', desc="basename for cache file") # internal identifier digest = Property( depends_on = ['source.digest', 'start', 'stop', \ 'invalid_channels']) @cached_property def _get_digest( self ): return digest(self) @cached_property def _get_basename( self ): if 'basename' in self.source.all_trait_names(): return self.source.basename else: return self.source.__class__.__name__ + self.source.digest @cached_property def _get_channels( self ): if len(self.invalid_channels)==0: return slice(0, None, None) allr=[i for i in range(self.numchannels_total) if not (i in self.invalid_channels)] return array(allr) @cached_property def _get_numchannels( self ): if len(self.invalid_channels)==0: return self.numchannels_total return len(self.channels) @cached_property def _get_numsamples( self ): sli = slice(self.start, self.stop).indices(self.numsamples_total) return sli[1]-sli[0] def result(self, num): """ Python generator that yields the output block-wise. Parameters ---------- num : integer This parameter defines the size of the blocks to be yielded (i.e. the number of samples per block). Returns ------- Samples in blocks of shape (num, :attr:`numchannels`). The last block may be shorter than num. """ sli = slice(self.start, self.stop).indices(self.numsamples_total) start = sli[0] stop = sli[1] if start >= stop: raise IOError("no samples available") if start != 0 or stop != self.numsamples_total: stopoff = -stop % num offset = -start % num if offset == 0: offset = num buf = empty((num + offset , self.numchannels), dtype=float) # buffer array i = 0 for block in self.source.result(num): i += num if i > start and i <= stop+stopoff: ns = block.shape[0] # numbers of samples buf[offset:offset+ns] = block[:, self.channels] if i > start + num: yield buf[:num] buf[:offset] = buf[num:num+offset] if offset-stopoff != 0: yield buf[:(offset-stopoff)] else: # if no start/stop given, don't do the resorting thing for block in self.source.result(num): yield block[:, self.channels]
class Calib(HasPrivateTraits): """ Container for calibration data in `*.xml` format This class serves as interface to load calibration data for the used microphone array. """ #: Name of the .xml file to be imported. from_file = File(filter=['*.xml'], desc="name of the xml file to import") #: Basename of the .xml-file. Readonly / is set automatically. basename = Property(depends_on='from_file', desc="basename of xml file") #: Number of microphones in the calibration data, #: is set automatically / read from file. num_mics = CLong(0, desc="number of microphones in the geometry") #: Array of calibration factors, #: is set automatically / read from file. data = CArray(desc="calibration data") # Internal identifier digest = Property(depends_on=[ 'basename', ]) @cached_property def _get_digest(self): return digest(self) @cached_property def _get_basename(self): if not path.isfile(self.from_file): return '' return path.splitext(path.basename(self.from_file))[0] @on_trait_change('basename') def import_data(self): """ Loads the calibration data from `*.xml` file . """ if not path.isfile(self.from_file): # empty calibration if self.basename == '': self.data = None self.num_mics = 0 # no file there else: self.data = array([ 1.0, ], 'd') self.num_mics = 1 return import xml.dom.minidom doc = xml.dom.minidom.parse(self.from_file) names = [] data = [] for element in doc.getElementsByTagName('pos'): names.append(element.getAttribute('Name')) data.append(float(element.getAttribute('factor'))) self.data = array(data, 'd') self.num_mics = self.data.shape[0]
class CoercibleLongTrait(HasTraits): value = CLong(LONG_TYPE(99))
class GenericSignalGenerator(SignalGenerator): """ Generate signal from output of :class:`~acoular.tprocess.SamplesGenerator` object. """ #: Data source; :class:`~acoular.tprocess.SamplesGenerator` or derived object. source = Trait(SamplesGenerator) #: Sampling frequency of output signal, as given by :attr:`source`. sample_freq = Delegate('source') _numsamples = CLong(0) #: Number of samples to generate. Is set to source.numsamples by default. numsamples = Property() def _get_numsamples(self): if self._numsamples: return self._numsamples else: return self.source.numsamples def _set_numsamples(self, numsamples): self._numsamples = numsamples #: Boolean flag, if 'True' (default), signal track is repeated if requested #: :attr:`numsamples` is higher than available sample number loop_signal = Bool(True) # internal identifier digest = Property( depends_on = ['source.digest', 'loop_signal', 'numsamples', \ 'rms', '__class__'], ) @cached_property def _get_digest(self): return digest(self) def signal(self): """ Deliver the signal. Returns ------- array of floats The resulting signal as an array of length :attr:`~GenericSignalGenerator.numsamples`. """ block = 1024 if self.source.numchannels > 1: warn( "Signal source has more than one channel. Only channel 0 will be used for signal.", Warning, stacklevel=2) nums = self.numsamples track = zeros(nums) # iterate through source generator to fill signal track for i, temp in enumerate(self.source.result(block)): start = block * i stop = start + len(temp[:, 0]) if nums > stop: track[start:stop] = temp[:, 0] else: # exit loop preliminarily if wanted signal samples are reached track[start:nums] = temp[:nums - start, 0] break # if the signal should be repeated after finishing and there are still samples open if self.loop_signal and (nums > stop): # fill up empty track with as many full source signals as possible nloops = nums // stop if nloops > 1: track[stop:stop * nloops] = tile(track[:stop], nloops - 1) # fill up remaining empty track res = nums % stop # last part of unfinished loop if res > 0: track[stop * nloops:] = track[:res] # The rms value is just an amplification here return self.rms * track