acoular 24.10__py3-none-any.whl → 25.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- acoular/__init__.py +5 -2
- acoular/aiaa/__init__.py +12 -0
- acoular/{tools → aiaa}/aiaa.py +23 -28
- acoular/base.py +75 -55
- acoular/calib.py +129 -34
- acoular/configuration.py +11 -9
- acoular/demo/__init__.py +1 -0
- acoular/demo/acoular_demo.py +29 -16
- acoular/deprecation.py +85 -0
- acoular/environments.py +31 -19
- acoular/fastFuncs.py +90 -84
- acoular/fbeamform.py +203 -411
- acoular/fprocess.py +49 -41
- acoular/grids.py +101 -143
- acoular/h5cache.py +29 -40
- acoular/h5files.py +2 -6
- acoular/microphones.py +50 -59
- acoular/process.py +366 -59
- acoular/sdinput.py +23 -20
- acoular/signals.py +116 -109
- acoular/sources.py +201 -240
- acoular/spectra.py +53 -229
- acoular/tbeamform.py +79 -202
- acoular/tfastfuncs.py +21 -21
- acoular/tools/__init__.py +2 -8
- acoular/tools/helpers.py +216 -2
- acoular/tools/metrics.py +4 -4
- acoular/tools/utils.py +106 -200
- acoular/tprocess.py +348 -309
- acoular/traitsviews.py +10 -10
- acoular/trajectory.py +7 -10
- acoular/version.py +2 -2
- {acoular-24.10.dist-info → acoular-25.1.dist-info}/METADATA +38 -17
- acoular-25.1.dist-info/RECORD +56 -0
- {acoular-24.10.dist-info → acoular-25.1.dist-info}/WHEEL +1 -1
- acoular-24.10.dist-info/RECORD +0 -54
- {acoular-24.10.dist-info → acoular-25.1.dist-info}/licenses/AUTHORS.rst +0 -0
- {acoular-24.10.dist-info → acoular-25.1.dist-info}/licenses/LICENSE +0 -0
acoular/process.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# ------------------------------------------------------------------------------
|
|
2
2
|
# Copyright (c) Acoular Development Team.
|
|
3
3
|
# ------------------------------------------------------------------------------
|
|
4
|
-
"""
|
|
4
|
+
"""General purpose blockwise processing methods independent of the domain (time or frequency).
|
|
5
5
|
|
|
6
6
|
.. autosummary::
|
|
7
7
|
:toctree: generated/
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
SampleSplitter
|
|
12
12
|
TimeAverage
|
|
13
13
|
TimeCache
|
|
14
|
+
SamplesBuffer
|
|
14
15
|
"""
|
|
15
16
|
|
|
16
17
|
import threading
|
|
@@ -18,13 +19,17 @@ from collections import deque
|
|
|
18
19
|
from inspect import currentframe
|
|
19
20
|
from warnings import warn
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
import numpy as np
|
|
23
|
+
from traits.api import Any, Array, Bool, Dict, Enum, Instance, Int, Property, Union, cached_property, on_trait_change
|
|
22
24
|
|
|
25
|
+
# acoular imports
|
|
23
26
|
from .base import Generator, InOut
|
|
24
27
|
from .configuration import config
|
|
28
|
+
from .deprecation import deprecated_alias
|
|
25
29
|
from .h5cache import H5cache
|
|
26
30
|
from .h5files import H5CacheFileBase
|
|
27
31
|
from .internal import digest
|
|
32
|
+
from .tools.utils import find_basename
|
|
28
33
|
|
|
29
34
|
|
|
30
35
|
class LockedGenerator:
|
|
@@ -42,40 +47,45 @@ class LockedGenerator:
|
|
|
42
47
|
return self.it.__next__()
|
|
43
48
|
|
|
44
49
|
|
|
50
|
+
@deprecated_alias({'naverage': 'num_per_average', 'numsamples': 'num_samples'}, read_only=['numsamples'])
|
|
45
51
|
class Average(InOut):
|
|
46
52
|
"""Calculates the average across consecutive time samples or frequency snapshots.
|
|
47
53
|
|
|
48
54
|
The average operation is performed differently depending on the source type.
|
|
49
|
-
If the source is a time domain source
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
the
|
|
55
|
+
If the source is a time domain source
|
|
56
|
+
(e.g. derived from :class:`~acoular.base.SamplesGenerator`), the average is
|
|
57
|
+
calculated over a certain number of time samples given by :attr:`num_per_average`.
|
|
58
|
+
If the source is a frequency domain source (e.g. derived from
|
|
59
|
+
:class:`~acoular.base.SpectraGenerator`), the average is calculated over a certain
|
|
60
|
+
number of snapshots given by :attr:`num_per_average`.
|
|
53
61
|
|
|
54
62
|
Examples
|
|
55
63
|
--------
|
|
56
|
-
For estimate the RMS of a white noise (time-domain) signal, the average of the squared
|
|
64
|
+
For estimate the RMS of a white noise (time-domain) signal, the average of the squared
|
|
65
|
+
signal can be calculated:
|
|
57
66
|
|
|
58
67
|
>>> import acoular as ac
|
|
59
68
|
>>> import numpy as np
|
|
60
69
|
>>>
|
|
61
|
-
>>> signal = ac.WNoiseGenerator(sample_freq=51200,
|
|
70
|
+
>>> signal = ac.WNoiseGenerator(sample_freq=51200, num_samples=51200, rms=2.0).signal()
|
|
62
71
|
>>> ts = ac.TimeSamples(data=signal[:, np.newaxis], sample_freq=51200)
|
|
63
72
|
>>> tp = ac.TimePower(source=ts)
|
|
64
|
-
>>> avg = ac.Average(source=tp,
|
|
73
|
+
>>> avg = ac.Average(source=tp, num_per_average=512)
|
|
65
74
|
>>> mean_squared_value = next(avg.result(num=1))
|
|
66
75
|
>>> rms = np.sqrt(mean_squared_value)[0, 0]
|
|
67
76
|
>>> print(rms)
|
|
68
77
|
1.9985200025816718
|
|
69
78
|
|
|
70
|
-
Here, each evaluation of the generator created by the :meth:`result` method of the
|
|
71
|
-
via the :meth:`next` function returns :code:`num=1` average across a
|
|
79
|
+
Here, each evaluation of the generator created by the :meth:`result` method of the
|
|
80
|
+
:class:`Average` object via the :meth:`next` function returns :code:`num=1` average across a
|
|
81
|
+
snapshot of 512 samples.
|
|
72
82
|
|
|
73
|
-
If the source is a frequency domain source, the average is calculated over a certain number
|
|
74
|
-
snapshots, defined by :attr:`
|
|
83
|
+
If the source is a frequency domain source, the average is calculated over a certain number
|
|
84
|
+
of snapshots, defined by :attr:`num_per_average`.
|
|
75
85
|
|
|
76
86
|
>>> fft = ac.RFFT(source=ts, block_size=64)
|
|
77
87
|
>>> ps = ac.AutoPowerSpectra(source=fft)
|
|
78
|
-
>>> avg = ac.Average(source=ps,
|
|
88
|
+
>>> avg = ac.Average(source=ps, num_per_average=16)
|
|
79
89
|
>>> mean_power = next(avg.result(num=1))
|
|
80
90
|
>>> print(np.sqrt(mean_power.sum()))
|
|
81
91
|
2.0024960894399295
|
|
@@ -87,17 +97,17 @@ class Average(InOut):
|
|
|
87
97
|
|
|
88
98
|
#: Number of samples (time domain source) or snapshots (frequency domain source)
|
|
89
99
|
#: to average over, defaults to 64.
|
|
90
|
-
|
|
100
|
+
num_per_average = Int(64, desc='number of samples/snapshots to average over')
|
|
91
101
|
|
|
92
102
|
#: Sampling frequency of the output signal, is set automatically.
|
|
93
|
-
sample_freq = Property(depends_on='source.sample_freq,
|
|
103
|
+
sample_freq = Property(depends_on=['source.sample_freq', 'num_per_average'])
|
|
94
104
|
|
|
95
105
|
#: Number of samples (time domain) or snapshots (frequency domain) of the output signal.
|
|
96
106
|
#: Is set automatically.
|
|
97
|
-
|
|
107
|
+
num_samples = Property(depends_on=['source.num_samples', 'num_per_average'])
|
|
98
108
|
|
|
99
109
|
# internal identifier
|
|
100
|
-
digest = Property(depends_on=['source.digest', '
|
|
110
|
+
digest = Property(depends_on=['source.digest', 'num_per_average'])
|
|
101
111
|
|
|
102
112
|
@cached_property
|
|
103
113
|
def _get_digest(self):
|
|
@@ -106,13 +116,13 @@ class Average(InOut):
|
|
|
106
116
|
@cached_property
|
|
107
117
|
def _get_sample_freq(self):
|
|
108
118
|
if self.source:
|
|
109
|
-
return 1.0 * self.source.sample_freq / self.
|
|
119
|
+
return 1.0 * self.source.sample_freq / self.num_per_average
|
|
110
120
|
return None
|
|
111
121
|
|
|
112
122
|
@cached_property
|
|
113
|
-
def
|
|
123
|
+
def _get_num_samples(self):
|
|
114
124
|
if self.source:
|
|
115
|
-
return self.source.
|
|
125
|
+
return self.source.num_samples / self.num_per_average
|
|
116
126
|
return None
|
|
117
127
|
|
|
118
128
|
def result(self, num):
|
|
@@ -127,11 +137,11 @@ class Average(InOut):
|
|
|
127
137
|
Returns
|
|
128
138
|
-------
|
|
129
139
|
Average of the output of source.
|
|
130
|
-
Yields samples in blocks of shape (num,
|
|
140
|
+
Yields samples in blocks of shape (num, num_channels).
|
|
131
141
|
The last block may be shorter than num.
|
|
132
142
|
|
|
133
143
|
"""
|
|
134
|
-
nav = self.
|
|
144
|
+
nav = self.num_per_average
|
|
135
145
|
for temp in self.source.result(num * nav):
|
|
136
146
|
ns, nc = temp.shape
|
|
137
147
|
nso = int(ns / nav)
|
|
@@ -158,7 +168,7 @@ class Cache(InOut):
|
|
|
158
168
|
>>> cache = ac.Cache(source=fft) # cache the output of the FFT in cache file
|
|
159
169
|
>>> for block in cache.result(num=1): # read the cached data block-wise
|
|
160
170
|
... print(block.shape)
|
|
161
|
-
[('
|
|
171
|
+
[('void_cache.h5', 1)]
|
|
162
172
|
(1, 513)
|
|
163
173
|
|
|
164
174
|
The caching behaviour can be controlled by the :class:`~acoular.configuration.Config` instance
|
|
@@ -172,13 +182,13 @@ class Cache(InOut):
|
|
|
172
182
|
"""
|
|
173
183
|
|
|
174
184
|
# basename for cache
|
|
175
|
-
basename = Property(depends_on='digest')
|
|
185
|
+
basename = Property(depends_on=['digest'])
|
|
176
186
|
|
|
177
187
|
# hdf5 cache file
|
|
178
188
|
h5f = Instance(H5CacheFileBase, transient=True)
|
|
179
189
|
|
|
180
190
|
# internal identifier
|
|
181
|
-
digest = Property(depends_on=['source.digest'
|
|
191
|
+
digest = Property(depends_on=['source.digest'])
|
|
182
192
|
|
|
183
193
|
@cached_property
|
|
184
194
|
def _get_digest(self):
|
|
@@ -186,17 +196,7 @@ class Cache(InOut):
|
|
|
186
196
|
|
|
187
197
|
@cached_property
|
|
188
198
|
def _get_basename(self):
|
|
189
|
-
|
|
190
|
-
basename = 'void' # if no file source is found
|
|
191
|
-
while obj:
|
|
192
|
-
if 'basename' in obj.all_trait_names(): # at original source?
|
|
193
|
-
basename = obj.basename # get the name
|
|
194
|
-
break
|
|
195
|
-
try:
|
|
196
|
-
obj = obj.source # traverse down until original data source
|
|
197
|
-
except AttributeError:
|
|
198
|
-
obj = None
|
|
199
|
-
return basename
|
|
199
|
+
return find_basename(self.source)
|
|
200
200
|
|
|
201
201
|
def _pass_data(self, num):
|
|
202
202
|
yield from self.source.result(num)
|
|
@@ -256,10 +256,10 @@ class Cache(InOut):
|
|
|
256
256
|
|
|
257
257
|
Returns
|
|
258
258
|
-------
|
|
259
|
-
Samples in blocks of shape (num,
|
|
259
|
+
Samples in blocks of shape (num, num_channels).
|
|
260
260
|
The last block may be shorter than num.
|
|
261
261
|
Echos the source output, but reads it from cache
|
|
262
|
-
when available and prevents
|
|
262
|
+
when available and prevents unnecessary recalculation.
|
|
263
263
|
|
|
264
264
|
"""
|
|
265
265
|
if config.global_caching == 'none':
|
|
@@ -282,8 +282,8 @@ class Cache(InOut):
|
|
|
282
282
|
elif not self.h5f.get_data_by_reference(nodename).attrs['complete']:
|
|
283
283
|
if config.global_caching == 'readonly':
|
|
284
284
|
warn(
|
|
285
|
-
"Cache file is incomplete for nodename
|
|
286
|
-
|
|
285
|
+
f"Cache file is incomplete for nodename {nodename}. With config.global_caching='readonly', \
|
|
286
|
+
the cache file will not be used!",
|
|
287
287
|
Warning,
|
|
288
288
|
stacklevel=1,
|
|
289
289
|
)
|
|
@@ -298,9 +298,74 @@ class Cache(InOut):
|
|
|
298
298
|
|
|
299
299
|
|
|
300
300
|
class SampleSplitter(InOut):
|
|
301
|
-
"""
|
|
302
|
-
|
|
303
|
-
|
|
301
|
+
"""
|
|
302
|
+
Distributes data from a source to several following objects in a block-wise manner.
|
|
303
|
+
|
|
304
|
+
The `SampleSplitter` class is designed to take data from a single
|
|
305
|
+
:class:`~acoular.base.Generator` derived source object and distribute it to multiple
|
|
306
|
+
:class:`~acoular.base.Generator` derived objects. For each object, the :class:`SampleSplitter`
|
|
307
|
+
holds a virtual block buffer from which the subsequently connected objects receive data in a
|
|
308
|
+
first-in-first-out (FIFO) manner. This allows for efficient data handling and processing in
|
|
309
|
+
parallel.
|
|
310
|
+
|
|
311
|
+
Examples
|
|
312
|
+
--------
|
|
313
|
+
Consider a time domain source signal stream from which the FFT spectra and the signal power
|
|
314
|
+
are calculated block-wise and in parallel by using the :class:`~acoular.fprocess.RFFT` as well
|
|
315
|
+
as the :class:`~acoular.tprocess.TimePower` and :class:`~acoular.process.Average`
|
|
316
|
+
objects. The `SampleSplitter` object is used to distribute the incoming blocks of data to the
|
|
317
|
+
`RFFT` and `TimePower` object buffers whenever one of these objects calls the :meth:`result`
|
|
318
|
+
generator.
|
|
319
|
+
For the `TimePower` object, the buffer size is set to 10 blocks. If the buffer is full, an error
|
|
320
|
+
is raised since the buffer overflow treatment is set to 'error'. For the `RFFT` object, the
|
|
321
|
+
block buffer size is set to 1 block, and the buffer overflow treatment is set to 'none'. This
|
|
322
|
+
is done to reduce latency in the FFT calculation, as the FFT calculation may take longer than
|
|
323
|
+
the signal power calculation. If new data is available and the block buffer for the `RFFT`
|
|
324
|
+
object is full, the `SampleSplitter` will drop the oldest block of data in the buffer. Thus, the
|
|
325
|
+
`RFFT` object will always receive the most recent block of data.
|
|
326
|
+
|
|
327
|
+
>>> import acoular as ac
|
|
328
|
+
>>> import numpy as np
|
|
329
|
+
>>>
|
|
330
|
+
>>> # create a time domain signal source
|
|
331
|
+
>>> ts = ac.TimeSamples(data=np.random.rand(1024, 1), sample_freq=51200)
|
|
332
|
+
>>>
|
|
333
|
+
>>> # create the sample splitter object
|
|
334
|
+
>>> ss = ac.SampleSplitter(source=ts)
|
|
335
|
+
>>>
|
|
336
|
+
>>> # create the FFT spectra and further objects that receive the data
|
|
337
|
+
>>> fft = ac.RFFT(source=ss, block_size=64)
|
|
338
|
+
>>> pow = ac.TimePower(source=ss)
|
|
339
|
+
>>> avg = ac.Average(source=pow, num_per_average=64)
|
|
340
|
+
>>>
|
|
341
|
+
>>> # register the subsequent processing block objects at the sample splitter
|
|
342
|
+
>>> ss.register_object(fft, buffer_size=1, buffer_overflow_treatment='none')
|
|
343
|
+
>>> ss.register_object(pow, buffer_size=10, buffer_overflow_treatment='error')
|
|
344
|
+
|
|
345
|
+
After object registration, the `SampleSplitter` object is ready to distribute the data to the
|
|
346
|
+
object buffers. The block buffers can be accessed via the `block_buffer` attribute of the
|
|
347
|
+
`SampleSplitter` object.
|
|
348
|
+
|
|
349
|
+
>>> ss.block_buffer.values()
|
|
350
|
+
dict_values([deque([], maxlen=1), deque([], maxlen=10)])
|
|
351
|
+
|
|
352
|
+
Calling the result method of the FFT object will start the data collection and distribution
|
|
353
|
+
process.
|
|
354
|
+
|
|
355
|
+
>>> generator = fft.result(num=1)
|
|
356
|
+
>>> fft_res = next(generator)
|
|
357
|
+
|
|
358
|
+
Although we haven't called the result method of the signal power object, one data block is
|
|
359
|
+
already available in the buffer.
|
|
360
|
+
|
|
361
|
+
>>> print(len(ss.block_buffer[pow]))
|
|
362
|
+
1
|
|
363
|
+
|
|
364
|
+
To remove registered objects from the `SampleSplitter`, use the :meth:`remove_object` method.
|
|
365
|
+
|
|
366
|
+
>>> ss.remove_object(pow)
|
|
367
|
+
>>> print(len(ss.block_buffer))
|
|
368
|
+
1
|
|
304
369
|
"""
|
|
305
370
|
|
|
306
371
|
#: dictionary with block buffers (dict values) of registered objects (dict
|
|
@@ -308,7 +373,13 @@ class SampleSplitter(InOut):
|
|
|
308
373
|
block_buffer = Dict(key_trait=Instance(Generator))
|
|
309
374
|
|
|
310
375
|
#: max elements/blocks in block buffers.
|
|
311
|
-
|
|
376
|
+
#: Can be set individually for each registered object.
|
|
377
|
+
#: Default is 100 blocks for each registered object.
|
|
378
|
+
buffer_size = Union(
|
|
379
|
+
Int,
|
|
380
|
+
Dict(key_trait=Instance(Generator), value_trait=Int),
|
|
381
|
+
default_value=100,
|
|
382
|
+
)
|
|
312
383
|
|
|
313
384
|
#: defines behaviour in case of block_buffer overflow. Can be set individually
|
|
314
385
|
#: for each registered object.
|
|
@@ -318,7 +389,7 @@ class SampleSplitter(InOut):
|
|
|
318
389
|
#: * 'none': nothing happens. Possibly leads to lost blocks of data
|
|
319
390
|
buffer_overflow_treatment = Dict(
|
|
320
391
|
key_trait=Instance(Generator),
|
|
321
|
-
value_trait=
|
|
392
|
+
value_trait=Enum('error', 'warning', 'none'),
|
|
322
393
|
desc='defines buffer overflow behaviour.',
|
|
323
394
|
)
|
|
324
395
|
|
|
@@ -330,13 +401,17 @@ class SampleSplitter(InOut):
|
|
|
330
401
|
_buffer_overflow = Bool(False)
|
|
331
402
|
|
|
332
403
|
# Helper Trait holds source generator
|
|
333
|
-
_source_generator =
|
|
404
|
+
_source_generator = Instance(LockedGenerator)
|
|
334
405
|
|
|
335
|
-
def _create_block_buffer(self, obj):
|
|
336
|
-
|
|
406
|
+
def _create_block_buffer(self, obj, buffer_size=None):
|
|
407
|
+
if buffer_size is None:
|
|
408
|
+
buffer_size = self.buffer_size if isinstance(self.buffer_size, int) else self.buffer_size[obj]
|
|
409
|
+
self.block_buffer[obj] = deque([], maxlen=buffer_size)
|
|
337
410
|
|
|
338
|
-
def _create_buffer_overflow_treatment(self, obj):
|
|
339
|
-
|
|
411
|
+
def _create_buffer_overflow_treatment(self, obj, buffer_overflow_treatment=None):
|
|
412
|
+
if buffer_overflow_treatment is None:
|
|
413
|
+
buffer_overflow_treatment = 'error'
|
|
414
|
+
self.buffer_overflow_treatment[obj] = buffer_overflow_treatment
|
|
340
415
|
|
|
341
416
|
def _clear_block_buffer(self, obj):
|
|
342
417
|
self.block_buffer[obj].clear()
|
|
@@ -349,7 +424,8 @@ class SampleSplitter(InOut):
|
|
|
349
424
|
|
|
350
425
|
def _assert_obj_registered(self, obj):
|
|
351
426
|
if obj not in self.block_buffer:
|
|
352
|
-
|
|
427
|
+
msg = f'calling object {obj} is not registered.'
|
|
428
|
+
raise OSError(msg)
|
|
353
429
|
|
|
354
430
|
def _get_objs_to_inspect(self):
|
|
355
431
|
return [obj for obj in self.buffer_overflow_treatment if self.buffer_overflow_treatment[obj] != 'none']
|
|
@@ -360,7 +436,7 @@ class SampleSplitter(InOut):
|
|
|
360
436
|
if self.buffer_overflow_treatment[obj] == 'error':
|
|
361
437
|
self._buffer_overflow = True
|
|
362
438
|
elif self.buffer_overflow_treatment[obj] == 'warning':
|
|
363
|
-
warn('overfilled buffer for object:
|
|
439
|
+
warn(f'overfilled buffer for object: {obj} data will get lost', UserWarning, stacklevel=1)
|
|
364
440
|
|
|
365
441
|
def _create_source_generator(self, num):
|
|
366
442
|
for obj in self.block_buffer:
|
|
@@ -379,15 +455,45 @@ class SampleSplitter(InOut):
|
|
|
379
455
|
self._remove_block_buffer(obj)
|
|
380
456
|
self._create_block_buffer(obj)
|
|
381
457
|
|
|
382
|
-
def register_object(self, *objects_to_register):
|
|
383
|
-
"""
|
|
458
|
+
def register_object(self, *objects_to_register, buffer_size=None, buffer_overflow_treatment=None):
|
|
459
|
+
"""Register one or multiple :class:`~acoular.base.Generator` objects to the SampleSplitter.
|
|
460
|
+
|
|
461
|
+
Creates a block buffer for each object and sets the buffer size and buffer
|
|
462
|
+
overflow treatment.
|
|
463
|
+
|
|
464
|
+
Parameters
|
|
465
|
+
----------
|
|
466
|
+
objects_to_register : Generator
|
|
467
|
+
One or multiple :class:`~acoular.base.Generator` derived objects to be registered.
|
|
468
|
+
buffer_size : int, optional
|
|
469
|
+
Maximum number of elements/blocks in block buffer. If not set, the default buffer size
|
|
470
|
+
of 100 blocks is used.
|
|
471
|
+
buffer_overflow_treatment : str, optional
|
|
472
|
+
Defines the behaviour in case of reaching the buffer size.
|
|
473
|
+
Can be set individually for each object. Possible values are 'error', 'warning', and
|
|
474
|
+
'none'. If not set, the default value is 'error'.
|
|
475
|
+
"""
|
|
384
476
|
for obj in objects_to_register:
|
|
385
477
|
if obj not in self.block_buffer:
|
|
386
|
-
self._create_block_buffer(obj)
|
|
387
|
-
self._create_buffer_overflow_treatment(obj)
|
|
478
|
+
self._create_block_buffer(obj, buffer_size)
|
|
479
|
+
self._create_buffer_overflow_treatment(obj, buffer_overflow_treatment)
|
|
480
|
+
else:
|
|
481
|
+
msg = f'object {obj} is already registered.'
|
|
482
|
+
raise OSError(msg)
|
|
388
483
|
|
|
389
484
|
def remove_object(self, *objects_to_remove):
|
|
390
|
-
"""Function that can be used to remove registered objects.
|
|
485
|
+
"""Function that can be used to remove registered objects.
|
|
486
|
+
|
|
487
|
+
If no objects are given, all registered objects are removed.
|
|
488
|
+
|
|
489
|
+
Parameters
|
|
490
|
+
----------
|
|
491
|
+
objects_to_remove : list
|
|
492
|
+
One or multiple :class:`~acoular.base.Generator` derived objects to be removed.
|
|
493
|
+
If not set, all registered objects are removed.
|
|
494
|
+
"""
|
|
495
|
+
if not objects_to_remove:
|
|
496
|
+
objects_to_remove = list(self.block_buffer.keys())
|
|
391
497
|
for obj in objects_to_remove:
|
|
392
498
|
self._remove_block_buffer(obj)
|
|
393
499
|
self._remove_buffer_overflow_treatment(obj)
|
|
@@ -403,7 +509,7 @@ class SampleSplitter(InOut):
|
|
|
403
509
|
|
|
404
510
|
Returns
|
|
405
511
|
-------
|
|
406
|
-
Samples in blocks of shape (num,
|
|
512
|
+
Samples in blocks of shape (num, num_channels).
|
|
407
513
|
Delivers a block of samples to the calling object.
|
|
408
514
|
The last block may be shorter than num.
|
|
409
515
|
|
|
@@ -462,3 +568,204 @@ class TimeCache(Cache):
|
|
|
462
568
|
DeprecationWarning,
|
|
463
569
|
stacklevel=2,
|
|
464
570
|
)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
class SamplesBuffer(InOut):
|
|
574
|
+
"""Handles buffering of samples from a source.
|
|
575
|
+
|
|
576
|
+
This class is used to buffer samples from a source and provide them in blocks
|
|
577
|
+
of a specified size. There are several usecases for this class, as demonstrated in
|
|
578
|
+
the following.
|
|
579
|
+
|
|
580
|
+
Examples
|
|
581
|
+
--------
|
|
582
|
+
Let us assume we want to draw blocks of 16 samples from our source, but we want to make sure
|
|
583
|
+
that we always have twice the number of samples in the buffer. We can achieve this simple
|
|
584
|
+
behaviour by using the following code:
|
|
585
|
+
|
|
586
|
+
>>> import acoular as ac
|
|
587
|
+
>>> import numpy as np
|
|
588
|
+
>>> # create a white noise source with 512 samples
|
|
589
|
+
>>> source = ac.TimeSamples(
|
|
590
|
+
... data=ac.WNoiseGenerator(
|
|
591
|
+
... sample_freq=64,
|
|
592
|
+
... num_samples=512,
|
|
593
|
+
... ).signal()[:, np.newaxis],
|
|
594
|
+
... sample_freq=64,
|
|
595
|
+
... )
|
|
596
|
+
>>> # create a buffer with a size of 32 samples
|
|
597
|
+
>>> buffer = ac.process.SamplesBuffer(source=source, length=32)
|
|
598
|
+
>>> # get the first block of 16 samples
|
|
599
|
+
>>> block = next(buffer.result(num=16))
|
|
600
|
+
>>> np.testing.assert_array_equal(block, source.data[:16])
|
|
601
|
+
|
|
602
|
+
Here, on the first call to the result method, the buffer will fill up by collecting blocks with
|
|
603
|
+
same size from the source. The buffer will then return the first block of 16 samples. On the
|
|
604
|
+
next call to the result method, the buffer will be filled again and returns the next block of 16
|
|
605
|
+
samples.
|
|
606
|
+
|
|
607
|
+
In some cases, we might want to draw a different number of samples from the source than we want
|
|
608
|
+
to return. This can be achieved by setting the `source_num` trait of the buffer. A special case
|
|
609
|
+
is the return of a variable number of samples. This is the case, for example, in the class
|
|
610
|
+
:class:`~acoular.tbeamform.BeamformerTimeTraj`, in which a different number of time samples is
|
|
611
|
+
required from the buffer for further delay-and-sum processing depending on the expected delay,
|
|
612
|
+
which can be vary for moving sources. At the same time, however, only 'num' samples should be
|
|
613
|
+
written to and removed from the buffer. This behavior can be achieved by setting the
|
|
614
|
+
`shift_index_by` trait to 'num' and by setting the `result_num` trait to the number of samples
|
|
615
|
+
that should be returned by the result function.
|
|
616
|
+
|
|
617
|
+
>>> buffer = ac.process.SamplesBuffer(source=source, length=32, result_num=20, shift_index_by='num')
|
|
618
|
+
>>> block_sizes = []
|
|
619
|
+
>>> block_sizes.append(
|
|
620
|
+
... next(buffer.result(num=16)).shape[0]
|
|
621
|
+
... ) # this time, the buffer will return 20 samples, but the buffer will only forget the first 16 samples
|
|
622
|
+
>>> buffer.result_num = 24
|
|
623
|
+
>>> block_sizes.append(
|
|
624
|
+
... next(buffer.result(num=16)).shape[0]
|
|
625
|
+
... ) # this time, the buffer will return 24 samples, but the buffer will only forget the first 16 samples
|
|
626
|
+
>>> np.testing.assert_array_equal(block_sizes, [20, 24])
|
|
627
|
+
""" # noqa: W505
|
|
628
|
+
|
|
629
|
+
#: number of samples that fit in the buffer
|
|
630
|
+
length = Int(desc='number of samples that fit in the buffer')
|
|
631
|
+
|
|
632
|
+
#: number of samples per block to obtain from the source. If 'None', use 'num' argument of
|
|
633
|
+
#: result method
|
|
634
|
+
source_num = Union(
|
|
635
|
+
None,
|
|
636
|
+
Int(),
|
|
637
|
+
default_value=None,
|
|
638
|
+
desc='number of samples to return from the source. If "None", use "num" argument of result method',
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
#: number of samples to return from the buffer. If 'None', use 'num' argument of result method
|
|
642
|
+
result_num = Union(
|
|
643
|
+
None,
|
|
644
|
+
Int(),
|
|
645
|
+
default_value=None,
|
|
646
|
+
desc="number of samples to return from the buffer. If 'None', use 'num' argument of result method",
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
#: index shift value for the buffer. If "result_num", buffer will return and forget 'result_num'
|
|
650
|
+
#: samples. If "num", buffer will return 'result_num' samples but will forget 'num' samples
|
|
651
|
+
shift_index_by = Enum(
|
|
652
|
+
('result_num', 'num'),
|
|
653
|
+
desc=(
|
|
654
|
+
'index shift value for the buffer. If "result_num", use "result_num" trait.'
|
|
655
|
+
' If "num", use "num" argument of result method'
|
|
656
|
+
),
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
#: current filling level of buffer
|
|
660
|
+
level = Property(desc='current filling level of buffer')
|
|
661
|
+
|
|
662
|
+
#: data type of the buffer elements
|
|
663
|
+
dtype = Any(desc='data type of the buffer')
|
|
664
|
+
|
|
665
|
+
# flag to indicate that the source is empty, for internal use
|
|
666
|
+
_empty_source = Bool(False, desc='flag to indicate that the source is empty')
|
|
667
|
+
|
|
668
|
+
# the buffer for processing
|
|
669
|
+
_buffer = Array(shape=(None, None), desc='buffer for block processing')
|
|
670
|
+
|
|
671
|
+
# current index in buffer
|
|
672
|
+
_index = Int(desc='current index in buffer')
|
|
673
|
+
|
|
674
|
+
def _get_level(self):
|
|
675
|
+
return self._buffer.shape[0] - self._index
|
|
676
|
+
|
|
677
|
+
def _create_new_buffer(self):
|
|
678
|
+
self._buffer = np.zeros((self.length, self.num_channels), dtype=self.dtype)
|
|
679
|
+
self._index = self.length
|
|
680
|
+
self._empty_source = False
|
|
681
|
+
|
|
682
|
+
def _write_to_buffer(self, data):
|
|
683
|
+
ns = data.shape[0]
|
|
684
|
+
self._buffer[0 : (self.length - ns)] = self._buffer[-(self.length - ns) :]
|
|
685
|
+
self._buffer[-ns:, :] = data.astype(self.dtype)
|
|
686
|
+
self._index -= ns
|
|
687
|
+
|
|
688
|
+
def increase_buffer(self, num):
|
|
689
|
+
"""Increase the buffer by 'num' samples.
|
|
690
|
+
|
|
691
|
+
Returns
|
|
692
|
+
-------
|
|
693
|
+
None
|
|
694
|
+
"""
|
|
695
|
+
ar = np.zeros((num, self.num_channels), dtype=self._buffer.dtype)
|
|
696
|
+
self._buffer = np.concatenate((ar, self._buffer), axis=0)
|
|
697
|
+
self._index += num
|
|
698
|
+
self.length += num
|
|
699
|
+
|
|
700
|
+
def read_from_buffer(self, num):
|
|
701
|
+
"""Read samples from the buffer.
|
|
702
|
+
|
|
703
|
+
Parameters
|
|
704
|
+
----------
|
|
705
|
+
num : int
|
|
706
|
+
number of samples to read from the buffer.
|
|
707
|
+
|
|
708
|
+
Returns
|
|
709
|
+
-------
|
|
710
|
+
numpy.ndarray
|
|
711
|
+
block of samples from the buffer
|
|
712
|
+
|
|
713
|
+
"""
|
|
714
|
+
rnum = num if self.result_num is None else self.result_num
|
|
715
|
+
rnum = rnum if self.level >= rnum else self.level
|
|
716
|
+
data = self._buffer[self._index : self._index + rnum]
|
|
717
|
+
if self.shift_index_by == 'result_num':
|
|
718
|
+
self._index += rnum
|
|
719
|
+
else:
|
|
720
|
+
self._index += num
|
|
721
|
+
return data
|
|
722
|
+
|
|
723
|
+
def fill_buffer(self, snum):
|
|
724
|
+
"""Fill the buffer with samples from the source.
|
|
725
|
+
|
|
726
|
+
Parameters
|
|
727
|
+
----------
|
|
728
|
+
snum : int
|
|
729
|
+
number of samples to return from the source.
|
|
730
|
+
|
|
731
|
+
Yields
|
|
732
|
+
------
|
|
733
|
+
None
|
|
734
|
+
"""
|
|
735
|
+
source_generator = self.source.result(snum)
|
|
736
|
+
while not self._empty_source:
|
|
737
|
+
while self._index >= snum:
|
|
738
|
+
if self.result_num is not None:
|
|
739
|
+
while self.result_num > self.length:
|
|
740
|
+
self.increase_buffer(snum)
|
|
741
|
+
try:
|
|
742
|
+
self._write_to_buffer(next(source_generator))
|
|
743
|
+
except StopIteration:
|
|
744
|
+
self._empty_source = True
|
|
745
|
+
break
|
|
746
|
+
yield
|
|
747
|
+
|
|
748
|
+
def result(self, num):
|
|
749
|
+
"""Return blocks of samples from the buffer.
|
|
750
|
+
|
|
751
|
+
Parameters
|
|
752
|
+
----------
|
|
753
|
+
num : int
|
|
754
|
+
number of samples to return.
|
|
755
|
+
|
|
756
|
+
Yields
|
|
757
|
+
------
|
|
758
|
+
numpy.ndarray
|
|
759
|
+
block of samples from the buffer
|
|
760
|
+
"""
|
|
761
|
+
self._create_new_buffer()
|
|
762
|
+
snum = num
|
|
763
|
+
if self.source_num is not None:
|
|
764
|
+
snum = self.source_num
|
|
765
|
+
for _ in self.fill_buffer(snum):
|
|
766
|
+
if self.level > 0:
|
|
767
|
+
yield self.read_from_buffer(num)
|
|
768
|
+
else:
|
|
769
|
+
break
|
|
770
|
+
while self.level > 0:
|
|
771
|
+
yield self.read_from_buffer(num)
|