acoular 25.7__py3-none-any.whl → 26.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/sources.py CHANGED
@@ -4,6 +4,12 @@
4
4
  """
5
5
  Measured multichannel data management and simulation of acoustic sources.
6
6
 
7
+ .. inheritance-diagram::
8
+ acoular.sources
9
+ :top-classes:
10
+ acoular.base.SamplesGenerator
11
+ :parts: 1
12
+
7
13
  .. autosummary::
8
14
  :toctree: generated/
9
15
 
@@ -31,33 +37,10 @@ from os import path
31
37
  from warnings import warn
32
38
 
33
39
  import numba as nb
34
- from numpy import any as npany
35
- from numpy import (
36
- arange,
37
- arctan2,
38
- array,
39
- ceil,
40
- complex128,
41
- cross,
42
- dot,
43
- empty,
44
- int64,
45
- mod,
46
- newaxis,
47
- ones,
48
- ones_like,
49
- pi,
50
- real,
51
- repeat,
52
- sqrt,
53
- tile,
54
- uint32,
55
- zeros,
56
- )
57
- from numpy import min as npmin
40
+ import numpy as np
41
+ import scipy.linalg as spla
58
42
  from numpy.fft import fft, ifft
59
- from scipy.linalg import norm
60
- from scipy.special import sph_harm, spherical_jn, spherical_yn
43
+ from scipy.special import sph_harm_y, spherical_jn, spherical_yn
61
44
  from traits.api import (
62
45
  Any,
63
46
  Bool,
@@ -77,14 +60,11 @@ from traits.api import (
77
60
  Union,
78
61
  cached_property,
79
62
  observe,
80
- on_trait_change,
81
63
  )
82
64
 
83
65
  from .base import SamplesGenerator
84
66
 
85
67
  # acoular imports
86
- from .calib import Calib
87
- from .deprecation import deprecated_alias
88
68
  from .environments import Environment
89
69
  from .h5files import H5FileBase, _get_h5file_class
90
70
  from .internal import digest, ldigest
@@ -110,7 +90,6 @@ def _fill_mic_signal_block(out, signal, rm, ind, blocksize, num_channels, up, pr
110
90
  for m in range(num_channels):
111
91
  out[b, m] = signal[int(0.5 + ind[0, m])] / rm[0, m]
112
92
  ind += up
113
- return out
114
93
 
115
94
 
116
95
  def spherical_hn1(n, z):
@@ -202,7 +181,7 @@ def get_radiation_angles(direction, mpos, sourceposition):
202
181
  --------
203
182
  :func:`numpy.linalg.norm` :
204
183
  Computes the norm of a vector.
205
- :func:`numpy.arctan2` :
184
+ :obj:`numpy.arctan2` :
206
185
  Computes the arctangent of two variables, preserving quadrant information.
207
186
 
208
187
  Notes
@@ -228,21 +207,21 @@ def get_radiation_angles(direction, mpos, sourceposition):
228
207
  array([4.71238898, 4.71238898])
229
208
  """
230
209
  # direction of the Spherical Harmonics
231
- direc = array(direction, dtype=float)
232
- direc = direc / norm(direc)
210
+ direc = np.array(direction, dtype=float)
211
+ direc = direc / spla.norm(direc)
233
212
  # distances
234
- source_to_mic_vecs = mpos - array(sourceposition).reshape((3, 1))
213
+ source_to_mic_vecs = mpos - np.array(sourceposition).reshape((3, 1))
235
214
  source_to_mic_vecs[2] *= -1 # invert z-axis (acoular) #-1
236
215
  # z-axis (acoular) -> y-axis (spherical)
237
216
  # y-axis (acoular) -> z-axis (spherical)
238
217
  # theta
239
- ele = arctan2(sqrt(source_to_mic_vecs[0] ** 2 + source_to_mic_vecs[2] ** 2), source_to_mic_vecs[1])
240
- ele += arctan2(sqrt(direc[0] ** 2 + direc[2] ** 2), direc[1])
241
- ele += pi * 0.5 # convert from [-pi/2, pi/2] to [0,pi] range
218
+ ele = np.arctan2(np.sqrt(source_to_mic_vecs[0] ** 2 + source_to_mic_vecs[2] ** 2), source_to_mic_vecs[1])
219
+ ele += np.arctan2(np.sqrt(direc[0] ** 2 + direc[2] ** 2), direc[1])
220
+ ele += np.pi * 0.5 # convert from [-pi/2, pi/2] to [0,pi] range
242
221
  # phi
243
- azi = arctan2(source_to_mic_vecs[2], source_to_mic_vecs[0])
244
- azi += arctan2(direc[2], direc[0])
245
- azi = mod(azi, 2 * pi)
222
+ azi = np.arctan2(source_to_mic_vecs[2], source_to_mic_vecs[0])
223
+ azi += np.arctan2(direc[2], direc[0])
224
+ azi = np.mod(azi, 2 * np.pi)
246
225
  return azi, ele
247
226
 
248
227
 
@@ -279,13 +258,13 @@ def get_modes(lOrder, direction, mpos, sourceposition=None): # noqa: N803
279
258
  --------
280
259
  :func:`get_radiation_angles` :
281
260
  Computes azimuth and elevation angles between microphones and the source.
282
- :obj:`scipy.special.sph_harm` : Computes spherical harmonic values.
261
+ :obj:`scipy.special.sph_harm_y` : Computes spherical harmonic values.
283
262
 
284
263
  Notes
285
264
  -----
286
265
  - The azimuth (``azi``) and elevation (``ele``) angles between the microphones and the source
287
266
  are calculated using the :func:`get_radiation_angles` function.
288
- - Spherical harmonics (``sph_harm``) are computed for each mode ``(l, m)``, where ``l`` is the
267
+ - Spherical harmonics (``sph_harm_y``) are computed for each mode ``(l, m)``, where ``l`` is the
289
268
  degree (ranging from ``0`` to ``lOrder``) and ``m`` is the order
290
269
  (ranging from ``-l`` to ``+l``).
291
270
  - For negative orders (`m < 0`), the conjugate of the spherical harmonic is computed and scaled
@@ -305,20 +284,19 @@ def get_modes(lOrder, direction, mpos, sourceposition=None): # noqa: N803
305
284
  >>> modes.shape
306
285
  (2, 9)
307
286
  """
308
- sourceposition = sourceposition if sourceposition is not None else array([0, 0, 0])
287
+ sourceposition = sourceposition if sourceposition is not None else np.array([0, 0, 0])
309
288
  azi, ele = get_radiation_angles(direction, mpos, sourceposition) # angles between source and mics
310
- modes = zeros((azi.shape[0], (lOrder + 1) ** 2), dtype=complex128)
289
+ modes = np.zeros((azi.shape[0], (lOrder + 1) ** 2), dtype=np.complex128)
311
290
  i = 0
312
291
  for lidx in range(lOrder + 1):
313
292
  for m in range(-lidx, lidx + 1):
314
- modes[:, i] = sph_harm(m, lidx, azi, ele)
293
+ modes[:, i] = sph_harm_y(m, lidx, ele, azi)
315
294
  if m < 0:
316
295
  modes[:, i] = modes[:, i].conj() * 1j
317
296
  i += 1
318
297
  return modes
319
298
 
320
299
 
321
- @deprecated_alias({'name': 'file'}, removal_version='25.10')
322
300
  class TimeSamples(SamplesGenerator):
323
301
  """
324
302
  Container for processing time data in ``*.h5`` or NumPy array format.
@@ -330,13 +308,12 @@ class TimeSamples(SamplesGenerator):
330
308
 
331
309
  See Also
332
310
  --------
333
- :class:`acoular.sources.MaskedTimeSamples` :
311
+ :class:`~acoular.sources.MaskedTimeSamples` :
334
312
  Extends the functionality of class :class:`TimeSamples` by enabling the definition of start
335
313
  and stop samples as well as the specification of invalid channels.
336
314
 
337
315
  Notes
338
316
  -----
339
- - If a calibration object is provided, calibrated time-domain data will be returned.
340
317
  - Metadata from the :attr:`HDF5 file<file>` can be accessed through the :attr:`metadata`
341
318
  attribute.
342
319
 
@@ -370,40 +347,34 @@ class TimeSamples(SamplesGenerator):
370
347
  """
371
348
 
372
349
  #: Full path to the ``.h5`` file containing time-domain data.
373
- file = Union(None, File(filter=['*.h5'], exists=True), desc='name of data file')
350
+ file = Union(None, File(filter=['*.h5'], exists=True))
374
351
 
375
352
  #: Basename of the ``.h5`` file, set automatically from the :attr:`file` attribute.
376
- basename = Property(depends_on=['file'], desc='basename of data file')
377
-
378
- #: Calibration data, an instance of the :class:`~acoular.calib.Calib` class.
379
- #: (optional; if provided, the time data will be calibrated.)
380
- calib = Instance(Calib, desc='Calibration data')
353
+ basename = Property(depends_on=['file'])
381
354
 
382
355
  #: Number of input channels in the time data, set automatically based on the
383
356
  #: :attr:`loaded data<file>` or :attr:`specified array<data>`.
384
- num_channels = CInt(0, desc='number of input channels')
357
+ num_channels = CInt(0)
385
358
 
386
359
  #: Total number of time-domain samples, set automatically based on the :attr:`loaded data<file>`
387
360
  #: or :attr:`specified array<data>`.
388
- num_samples = CInt(0, desc='number of samples')
361
+ num_samples = CInt(0)
389
362
 
390
363
  #: A 2D NumPy array containing the time-domain data, shape (:attr:`num_samples`,
391
364
  #: :attr:`num_channels`).
392
- data = Any(transient=True, desc='the actual time data array')
365
+ data = Any(transient=True)
393
366
 
394
367
  #: HDF5 file object.
395
368
  h5f = Instance(H5FileBase, transient=True)
396
369
 
397
370
  #: Metadata loaded from the HDF5 file, if available.
398
- metadata = Dict(desc='metadata contained in .h5 file')
371
+ metadata = Dict()
399
372
 
400
373
  # Checksum over first data entries of all channels
401
- _datachecksum = Property()
374
+ _datachecksum = Property(depends_on=['data'])
402
375
 
403
376
  #: A unique identifier for the samples, based on its properties. (read-only)
404
- digest = Property(
405
- depends_on=['basename', 'calib.digest', '_datachecksum', 'sample_freq', 'num_channels', 'num_samples']
406
- )
377
+ digest = Property(depends_on=['basename', '_datachecksum', 'sample_freq', 'num_channels', 'num_samples'])
407
378
 
408
379
  def _get__datachecksum(self):
409
380
  return self.data[0, :].sum()
@@ -416,8 +387,8 @@ class TimeSamples(SamplesGenerator):
416
387
  def _get_basename(self):
417
388
  return get_file_basename(self.file)
418
389
 
419
- @on_trait_change('basename')
420
- def _load_data(self):
390
+ @observe('basename')
391
+ def _load_data(self, event): # noqa ARG002
421
392
  # Open the .h5 file and set attributes.
422
393
  if self.h5f is not None:
423
394
  with contextlib.suppress(OSError):
@@ -427,8 +398,8 @@ class TimeSamples(SamplesGenerator):
427
398
  self._load_timedata()
428
399
  self._load_metadata()
429
400
 
430
- @on_trait_change('data')
431
- def _load_shapes(self):
401
+ @observe('data')
402
+ def _load_shapes(self, event): # noqa ARG002
432
403
  # Set :attr:`num_channels` and :attr:`num_samples` from data.
433
404
  if self.data is not None:
434
405
  self.num_samples, self.num_channels = self.data.shape
@@ -450,8 +421,7 @@ class TimeSamples(SamplesGenerator):
450
421
 
451
422
  The :meth:`result` method is a Python generator that yields blocks of time-domain data
452
423
  of the specified size. Data is either read from an HDF5 file (if :attr:`file` is set)
453
- or from a NumPy array (if :attr:`data` is directly provided). If a calibration object
454
- is specified, the returned data is calibrated.
424
+ or from a NumPy array (if :attr:`data` is directly provided).
455
425
 
456
426
  Parameters
457
427
  ----------
@@ -470,14 +440,6 @@ class TimeSamples(SamplesGenerator):
470
440
  ------
471
441
  :obj:`OSError`
472
442
  If no samples are available (i.e., :attr:`num_samples` is ``0``).
473
- :obj:`ValueError`
474
- If the calibration data does not match the number of channels.
475
-
476
- Warnings
477
- --------
478
- A deprecation warning is raised if the calibration functionality is used directly in
479
- :class:`TimeSamples`. Instead, the :class:`~acoular.calib.Calib` class should be used as a
480
- separate processing block.
481
443
 
482
444
  Examples
483
445
  --------
@@ -501,36 +463,11 @@ class TimeSamples(SamplesGenerator):
501
463
  raise OSError(msg)
502
464
  self._datachecksum # trigger checksum calculation # noqa: B018
503
465
  i = 0
504
- if self.calib:
505
- warn(
506
- 'The use of the calibration functionality in TimeSamples is deprecated and will be removed in \
507
- Acoular 25.10. Use the Calib class as an additional processing block instead.',
508
- DeprecationWarning,
509
- stacklevel=2,
510
- )
511
- if self.calib.num_mics == self.num_channels:
512
- cal_factor = self.calib.data[newaxis]
513
- else:
514
- msg = f'calibration data not compatible: {self.calib.num_mics:d}, {self.num_channels:d}'
515
- raise ValueError(msg)
516
- while i < self.num_samples:
517
- yield self.data[i : i + num] * cal_factor
518
- i += num
519
- else:
520
- while i < self.num_samples:
521
- yield self.data[i : i + num]
522
- i += num
523
-
524
-
525
- @deprecated_alias(
526
- {
527
- 'numchannels_total': 'num_channels_total',
528
- 'numsamples_total': 'num_samples_total',
529
- 'numchannels': 'num_channels',
530
- 'numsamples': 'num_samples',
531
- },
532
- read_only=['numchannels', 'numsamples'],
533
- )
466
+ while i < self.num_samples:
467
+ yield self.data[i : num + i]
468
+ i += num
469
+
470
+
534
471
  class MaskedTimeSamples(TimeSamples):
535
472
  """
536
473
  Container to process and manage time-domain data with support for masking samples and channels.
@@ -543,7 +480,7 @@ class MaskedTimeSamples(TimeSamples):
543
480
 
544
481
  See Also
545
482
  --------
546
- :class:`acoular.sources.TimeSamples` : The parent class for managing unmasked time-domain data.
483
+ :class:`~acoular.sources.TimeSamples` : The parent class for managing unmasked time-domain data.
547
484
 
548
485
  Notes
549
486
  -----
@@ -579,38 +516,34 @@ class MaskedTimeSamples(TimeSamples):
579
516
  """
580
517
 
581
518
  #: Index of the first sample to be considered valid. Default is ``0``.
582
- start = CInt(0, desc='start of valid samples')
519
+ start = CInt(0)
583
520
 
584
521
  #: Index of the last sample to be considered valid. If ``None``, all remaining samples from the
585
522
  #: :attr:`start` index onward are considered valid. Default is ``None``.
586
- stop = Union(None, CInt, desc='stop of valid samples')
523
+ stop = Union(None, CInt)
587
524
 
588
525
  #: List of channel indices to be excluded from processing. Default is ``[]``.
589
- invalid_channels = List(int, desc='list of invalid channels')
526
+ invalid_channels = List(int)
590
527
 
591
528
  #: A mask or index array representing valid channels. Automatically updated based on the
592
529
  #: :attr:`invalid_channels` and :attr:`num_channels_total` attributes.
593
- channels = Property(depends_on=['invalid_channels', 'num_channels_total'], desc='channel mask')
530
+ channels = Property(depends_on=['invalid_channels', 'num_channels_total'])
594
531
 
595
532
  #: Total number of input channels, including invalid channels. (read-only).
596
- num_channels_total = CInt(0, desc='total number of input channels')
533
+ num_channels_total = CInt(0)
597
534
 
598
535
  #: Total number of samples, including invalid samples. (read-only).
599
- num_samples_total = CInt(0, desc='total number of samples per channel')
536
+ num_samples_total = CInt(0)
600
537
 
601
538
  #: Number of valid input channels after excluding :attr:`invalid_channels`. (read-only)
602
- num_channels = Property(
603
- depends_on=['invalid_channels', 'num_channels_total'], desc='number of valid input channels'
604
- )
539
+ num_channels = Property(depends_on=['invalid_channels', 'num_channels_total'])
605
540
 
606
541
  #: Number of valid time-domain samples, based on :attr:`start` and :attr:`stop` indices.
607
542
  #: (read-only)
608
- num_samples = Property(
609
- depends_on=['start', 'stop', 'num_samples_total'], desc='number of valid samples per channel'
610
- )
543
+ num_samples = Property(depends_on=['start', 'stop', 'num_samples_total'])
611
544
 
612
545
  #: A unique identifier for the samples, based on its properties. (read-only)
613
- digest = Property(depends_on=['basename', 'start', 'stop', 'calib.digest', 'invalid_channels', '_datachecksum'])
546
+ digest = Property(depends_on=['basename', 'start', 'stop', 'invalid_channels', '_datachecksum'])
614
547
 
615
548
  @cached_property
616
549
  def _get_digest(self):
@@ -621,7 +554,7 @@ class MaskedTimeSamples(TimeSamples):
621
554
  if len(self.invalid_channels) == 0:
622
555
  return slice(0, None, None)
623
556
  allr = [i for i in range(self.num_channels_total) if i not in self.invalid_channels]
624
- return array(allr)
557
+ return np.array(allr)
625
558
 
626
559
  @cached_property
627
560
  def _get_num_channels(self):
@@ -634,8 +567,8 @@ class MaskedTimeSamples(TimeSamples):
634
567
  sli = slice(self.start, self.stop).indices(self.num_samples_total)
635
568
  return sli[1] - sli[0]
636
569
 
637
- @on_trait_change('basename')
638
- def _load_data(self):
570
+ @observe('basename')
571
+ def _load_data(self, event): # noqa ARG002
639
572
  # Open the .h5 file and set attributes.
640
573
  if not path.isfile(self.file):
641
574
  # no file there
@@ -650,8 +583,8 @@ class MaskedTimeSamples(TimeSamples):
650
583
  self._load_timedata()
651
584
  self._load_metadata()
652
585
 
653
- @on_trait_change('data')
654
- def _load_shapes(self):
586
+ @observe('data')
587
+ def _load_shapes(self, event): # noqa ARG002
655
588
  # Set :attr:`num_channels` and num_samples from :attr:`~acoular.sources.TimeSamples.data`.
656
589
  if self.data is not None:
657
590
  self.num_samples_total, self.num_channels_total = self.data.shape
@@ -667,8 +600,7 @@ class MaskedTimeSamples(TimeSamples):
667
600
  Generate blocks of valid time-domain data iteratively.
668
601
 
669
602
  The :meth:`result` method is a Python generator that yields blocks of valid time-domain data
670
- based on the specified :attr:`start` and :attr:`stop` indices and the valid channels. Data
671
- can be calibrated if a calibration object, given by :attr:`calib`, is provided.
603
+ based on the specified :attr:`start` and :attr:`stop` indices and the valid channels.
672
604
 
673
605
  Parameters
674
606
  ----------
@@ -688,15 +620,6 @@ class MaskedTimeSamples(TimeSamples):
688
620
  :obj:`OSError`
689
621
  If no valid samples are available (i.e., :attr:`start` and :attr:`stop` indices result
690
622
  in an empty range).
691
- :obj:`ValueError`
692
- If the :attr:`calibration data<calib>` is incompatible with the
693
- :attr:`number of valid channels<num_channels>`.
694
-
695
- Warnings
696
- --------
697
- A deprecation warning is raised if the calibration functionality is used directly in
698
- :class:`MaskedTimeSamples`. Instead, the :class:`acoular.calib.Calib` class should be used
699
- as a separate processing block.
700
623
 
701
624
  Examples
702
625
  --------
@@ -721,33 +644,15 @@ class MaskedTimeSamples(TimeSamples):
721
644
  sli = slice(self.start, self.stop).indices(self.num_samples_total)
722
645
  i = sli[0]
723
646
  stop = sli[1]
724
- cal_factor = 1.0
725
647
  if i >= stop:
726
648
  msg = 'no samples available'
727
649
  raise OSError(msg)
728
650
  self._datachecksum # trigger checksum calculation # noqa: B018
729
- if self.calib:
730
- warn(
731
- 'The use of the calibration functionality in MaskedTimeSamples is deprecated and will be removed in \
732
- Acoular 25.10. Use the Calib class as an additional processing block instead.',
733
- DeprecationWarning,
734
- stacklevel=2,
735
- )
736
- if self.calib.num_mics == self.num_channels_total:
737
- cal_factor = self.calib.data[self.channels][newaxis]
738
- elif self.calib.num_mics == self.num_channels:
739
- cal_factor = self.calib.data[newaxis]
740
- elif self.calib.num_mics == 0:
741
- warn('No calibration data used.', Warning, stacklevel=2)
742
- else:
743
- msg = f'calibration data not compatible: {self.calib.num_mics:d}, {self.num_channels:d}'
744
- raise ValueError(msg)
745
651
  while i < stop:
746
- yield self.data[i : min(i + num, stop)][:, self.channels] * cal_factor
652
+ yield self.data[i : min(i + num, stop)][:, self.channels]
747
653
  i += num
748
654
 
749
655
 
750
- @deprecated_alias({'numchannels': 'num_channels', 'numsamples': 'num_samples'}, read_only=True, removal_version='25.10')
751
656
  class PointSource(SamplesGenerator):
752
657
  """
753
658
  Define a fixed point source emitting a signal, intended for simulations.
@@ -759,9 +664,9 @@ class PointSource(SamplesGenerator):
759
664
 
760
665
  See Also
761
666
  --------
762
- :class:`acoular.signals.SignalGenerator` : For defining custom emitted signals.
763
- :class:`acoular.microphones.MicGeom` : For specifying microphone geometries.
764
- :class:`acoular.environments.Environment` : For modeling sound propagation effects.
667
+ :class:`~acoular.signals.SignalGenerator` : For defining custom emitted signals.
668
+ :class:`~acoular.microphones.MicGeom` : For specifying microphone geometries.
669
+ :class:`~acoular.environments.Environment` : For modeling sound propagation effects.
765
670
 
766
671
  Notes
767
672
  -----
@@ -815,19 +720,27 @@ class PointSource(SamplesGenerator):
815
720
  #: Instance of the :class:`~acoular.signals.SignalGenerator` class defining the emitted signal.
816
721
  signal = Instance(SignalGenerator)
817
722
 
818
- #: Coordinates ``(x, y, z)`` of the source in a left-oriented system. Default is
819
- #: ``(0.0, 0.0, 1.0)``.
820
- loc = Tuple((0.0, 0.0, 1.0), desc='source location')
723
+ #: Coordinates ``(x, y, z)`` of the source. Default is ``np.array([[0.0], [0.0], [1.0]])``.
724
+ loc = Property(CArray, desc='source location')
725
+
726
+ _loc = CArray(shape=(3, 1), value=np.array([[0.0], [0.0], [1.0]]), dtype=float)
727
+
728
+ def _get_loc(self):
729
+ return self._loc
730
+
731
+ def _set_loc(self, value):
732
+ value = np.asarray(value, dtype=float).reshape((3, 1))
733
+ self._loc = value
821
734
 
822
735
  #: Number of output channels, automatically set based on the :attr:`microphone geometry<mics>`.
823
736
  num_channels = Delegate('mics', 'num_mics')
824
737
 
825
738
  #: :class:`~acoular.microphones.MicGeom` object defining the positions of the microphones.
826
- mics = Instance(MicGeom, desc='microphone geometry')
739
+ mics = Instance(MicGeom)
827
740
 
828
741
  def _validate_locations(self):
829
- dist = self.env._r(array(self.loc).reshape((3, 1)), self.mics.pos)
830
- if npany(dist < 1e-7):
742
+ dist = self.env._r(self.loc, self.mics.pos)
743
+ if np.any(dist < 1e-7):
831
744
  warn('Source and microphone locations are identical.', Warning, stacklevel=2)
832
745
 
833
746
  #: An :class:`~acoular.environments.Environment` or derived object providing sound propagation
@@ -836,10 +749,10 @@ class PointSource(SamplesGenerator):
836
749
  env = Instance(Environment, args=())
837
750
 
838
751
  #: Start time of the signal in seconds. Default is ``0.0``.
839
- start_t = Float(0.0, desc='signal start time')
752
+ start_t = Float(0.0)
840
753
 
841
754
  #: Start time of data acquisition at the microphones in seconds. Default is ``0.0``.
842
- start = Float(0.0, desc='sample start time')
755
+ start = Float(0.0)
843
756
 
844
757
  #: Behavior of the signal for negative time indices,
845
758
  #: i.e. if (:attr:`start` ``<`` :attr:`start_t`):
@@ -848,10 +761,10 @@ class PointSource(SamplesGenerator):
848
761
  #: - ``'zeros'``: Use zeros, recommended for deterministic signals.
849
762
  #:
850
763
  #: Default is ``'loop'``.
851
- prepadding = Enum('loop', 'zeros', desc='Behaviour for negative time indices.')
764
+ prepadding = Enum('loop', 'zeros')
852
765
 
853
766
  #: Internal upsampling factor for finer signal resolution. Default is ``16``.
854
- up = Int(16, desc='upsampling factor')
767
+ up = Int(16)
855
768
 
856
769
  #: Total number of samples in the emitted signal, derived from the :attr:`signal` generator.
857
770
  num_samples = Delegate('signal')
@@ -905,31 +818,32 @@ class PointSource(SamplesGenerator):
905
818
  If signal processing or propagation cannot be performed.
906
819
  """
907
820
  self._validate_locations()
908
- N = int(ceil(self.num_samples / num)) # number of output blocks
821
+ num_samples_estimate = self.num_samples + (self.start_t - self.start) * self.sample_freq
822
+ N = int(np.ceil(num_samples_estimate / num)) # number of output blocks
909
823
  signal = self.signal.usignal(self.up)
910
- out = empty((num, self.num_channels))
824
+ out = np.empty((num, self.num_channels))
911
825
  # distances
912
- rm = self.env._r(array(self.loc).reshape((3, 1)), self.mics.pos).reshape(1, -1)
826
+ rm = self.env._r(self.loc, self.mics.pos).reshape(1, -1)
913
827
  # emission time relative to start_t (in samples) for first sample
914
828
  ind = (-rm / self.env.c - self.start_t + self.start) * self.sample_freq * self.up
915
829
 
916
830
  if self.prepadding == 'zeros':
917
831
  # number of blocks where signal behaviour is amended
918
- pre = -int(npmin(ind[0]) // (self.up * num))
832
+ pre = -int(np.min(ind[0]) // (self.up * num))
919
833
  # amend signal for first blocks
920
834
  # if signal stops during prepadding, terminate
921
835
  if pre >= N:
922
836
  for _nb in range(N - 1):
923
- out = _fill_mic_signal_block(out, signal, rm, ind, num, self.num_channels, self.up, True)
837
+ _fill_mic_signal_block(out, signal, rm, ind, num, self.num_channels, self.up, True)
924
838
  yield out
925
839
 
926
840
  blocksize = self.num_samples % num or num
927
- out = _fill_mic_signal_block(out, signal, rm, ind, blocksize, self.num_channels, self.up, True)
841
+ _fill_mic_signal_block(out, signal, rm, ind, blocksize, self.num_channels, self.up, True)
928
842
  yield out[:blocksize]
929
843
  return
930
844
  else:
931
845
  for _nb in range(pre):
932
- out = _fill_mic_signal_block(out, signal, rm, ind, num, self.num_channels, self.up, True)
846
+ _fill_mic_signal_block(out, signal, rm, ind, num, self.num_channels, self.up, True)
933
847
  yield out
934
848
 
935
849
  else:
@@ -937,12 +851,12 @@ class PointSource(SamplesGenerator):
937
851
 
938
852
  # main generator
939
853
  for _nb in range(N - pre - 1):
940
- out = _fill_mic_signal_block(out, signal, rm, ind, num, self.num_channels, self.up, False)
854
+ _fill_mic_signal_block(out, signal, rm, ind, num, self.num_channels, self.up, False)
941
855
  yield out
942
856
 
943
857
  # last block of variable size
944
858
  blocksize = self.num_samples % num or num
945
- out = _fill_mic_signal_block(out, signal, rm, ind, blocksize, self.num_channels, self.up, False)
859
+ _fill_mic_signal_block(out, signal, rm, ind, blocksize, self.num_channels, self.up, False)
946
860
  yield out[:blocksize]
947
861
 
948
862
 
@@ -959,18 +873,18 @@ class SphericalHarmonicSource(PointSource):
959
873
  """
960
874
 
961
875
  #: Order of the spherical harmonic representation. Default is ``0``.
962
- lOrder = Int(0, desc='Order of spherical harmonic') # noqa: N815
876
+ lOrder = Int(0) # noqa: N815
963
877
 
964
878
  #: Coefficients of the spherical harmonic modes for the given :attr:`lOrder`.
965
- alpha = CArray(desc='coefficients of the (lOrder,) spherical harmonic mode')
879
+ alpha = CArray()
966
880
 
967
881
  #: Vector defining the orientation of the spherical harmonic source. Default is
968
882
  #: ``(1.0, 0.0, 0.0)``.
969
- direction = Tuple((1.0, 0.0, 0.0), desc='Spherical Harmonic orientation')
883
+ direction = Tuple((1.0, 0.0, 0.0))
970
884
 
971
885
  #: Behavior of the signal for negative time indices. Currently only supports `loop`. Default is
972
886
  #: ``'loop'``.
973
- prepadding = Enum('loop', desc='Behaviour for negative time indices.')
887
+ prepadding = Enum('loop')
974
888
 
975
889
  # Unique identifier for the current state of the source, based on its properties. (read-only)
976
890
  digest = Property(
@@ -1026,9 +940,9 @@ class SphericalHarmonicSource(PointSource):
1026
940
  lOrder=self.lOrder,
1027
941
  direction=self.direction,
1028
942
  mpos=self.mics.pos,
1029
- sourceposition=array(self.loc),
943
+ sourceposition=np.array(self.loc),
1030
944
  )
1031
- return real(ifft(fft(signals, axis=0) * (Y_lm @ self.alpha), axis=0))
945
+ return np.real(ifft(fft(signals, axis=0) * (Y_lm @ self.alpha), axis=0))
1032
946
 
1033
947
  def result(self, num=128):
1034
948
  """
@@ -1060,15 +974,15 @@ class SphericalHarmonicSource(PointSource):
1060
974
 
1061
975
  signal = self.signal.usignal(self.up)
1062
976
  # emission time relative to start_t (in samples) for first sample
1063
- rm = self.env._r(array(self.loc).reshape((3, 1)), self.mics.pos)
1064
- ind = (-rm / self.env.c - self.start_t + self.start) * self.sample_freq + pi / 30
977
+ rm = self.env._r(self.loc, self.mics.pos)
978
+ ind = (-rm / self.env.c - self.start_t + self.start) * self.sample_freq + np.pi / 30
1065
979
  i = 0
1066
980
  n = self.num_samples
1067
- out = empty((num, self.num_channels))
981
+ out = np.empty((num, self.num_channels))
1068
982
  while n:
1069
983
  n -= 1
1070
984
  try:
1071
- out[i] = signal[array(0.5 + ind * self.up, dtype=int64)] / rm
985
+ out[i] = signal[np.array(0.5 + ind * self.up, dtype=np.int64)] / rm
1072
986
  ind += 1
1073
987
  i += 1
1074
988
  if i == num:
@@ -1091,22 +1005,22 @@ class MovingPointSource(PointSource):
1091
1005
 
1092
1006
  See Also
1093
1007
  --------
1094
- :class:`acoular.sources.PointSource` : For modeling stationary point sources.
1095
- :class:`acoular.trajectory.Trajectory` : For specifying source motion paths.
1008
+ :class:`~acoular.sources.PointSource` : For modeling stationary point sources.
1009
+ :class:`~acoular.trajectory.Trajectory` : For specifying source motion paths.
1096
1010
  """
1097
1011
 
1098
1012
  #: Determines whether convective amplification is considered. When ``True``, the amplitude of
1099
1013
  #: the signal is adjusted based on the relative motion between the source and microphones.
1100
1014
  #: Default is ``False``.
1101
- conv_amp = Bool(False, desc='determines if convective amplification is considered')
1015
+ conv_amp = Bool(False)
1102
1016
 
1103
1017
  #: Instance of the :class:`~acoular.trajectory.Trajectory` class specifying the source's motion.
1104
1018
  #: The trajectory defines the source's position and velocity at any given time.
1105
- trajectory = Instance(Trajectory, desc='trajectory of the source')
1019
+ trajectory = Instance(Trajectory)
1106
1020
 
1107
1021
  #: Behavior of the signal for negative time indices. Currently only supports ``'loop'``.
1108
1022
  #: Default is ``'loop'``.
1109
- prepadding = Enum('loop', desc='Behaviour for negative time indices.')
1023
+ prepadding = Enum('loop')
1110
1024
 
1111
1025
  #: A unique identifier for the current state of the source, based on its properties. (read-only)
1112
1026
  digest = Property(
@@ -1127,6 +1041,51 @@ class MovingPointSource(PointSource):
1127
1041
  def _get_digest(self):
1128
1042
  return digest(self)
1129
1043
 
1044
+ def get_moving_direction(self, direction, time=0):
1045
+ """
1046
+ Calculate the moving direction of the source along its trajectory.
1047
+
1048
+ This method computes the updated direction vector for the moving source, considering both
1049
+ translation along the :attr:`~MovingPointSource.trajectory` and rotation defined by the
1050
+ :attr:`reference vector<rvec>`. If the :attr:`reference vector<rvec>` is `(0, 0, 0)`, only
1051
+ translation is applied. Otherwise, the method incorporates rotation into the calculation.
1052
+
1053
+ Parameters
1054
+ ----------
1055
+ direction : :class:`numpy.ndarray`
1056
+ The initial orientation of the source, specified as a three-dimensional array.
1057
+ time : :class:`float`, optional
1058
+ The time at which the :attr:`~MovingPointSource.trajectory` position and velocity are
1059
+ evaluated. Defaults to ``0``.
1060
+
1061
+ Returns
1062
+ -------
1063
+ :class:`numpy.ndarray`
1064
+ The updated direction vector of the moving source after translation and, if applicable,
1065
+ rotation. The output is a three-dimensional array.
1066
+
1067
+ Notes
1068
+ -----
1069
+ - The method computes the translation direction vector based on the
1070
+ :attr:`~MovingPointSource.trajectory`'s velocity at the specified time.
1071
+ - If the :attr:`reference vector<rvec>` is non-zero, the method constructs a rotation matrix
1072
+ to compute the new source direction based on the
1073
+ :attr:`~MovingPointSource.trajectory`'s motion and the :attr:`reference vector<rvec>`.
1074
+ - The rotation matrix ensures that the new orientation adheres to the right-hand rule and
1075
+ remains orthogonal.
1076
+ """
1077
+ trajg1 = np.array(self.trajectory.location(time, der=1))[:, 0][:, np.newaxis]
1078
+ rflag = (self.rvec == 0).all() # flag translation vs. rotation
1079
+ if rflag:
1080
+ return direction
1081
+ dx = np.array(trajg1.T) # direction vector (new x-axis)
1082
+ dy = np.cross(self.rvec, dx) # new y-axis
1083
+ dz = np.cross(dx, dy) # new z-axis
1084
+ RM = np.array((dx, dy, dz)).T # rotation matrix
1085
+ RM /= np.sqrt((RM * RM).sum(0)) # column normalized
1086
+ newdir = np.dot(RM, direction)
1087
+ return np.cross(newdir[:, 0].T, self.rvec.T).T
1088
+
1130
1089
  def result(self, num=128):
1131
1090
  """
1132
1091
  Generate the output signal at microphones in blocks, accounting for source motion.
@@ -1167,25 +1126,25 @@ class MovingPointSource(PointSource):
1167
1126
  signal = self.signal.usignal(self.up)
1168
1127
  # shortcuts and initial values
1169
1128
  num_mics = self.num_channels
1170
- mpos = self.mics.pos[:, :, newaxis]
1171
- t = self.start + ones(num_mics)[:, newaxis] * arange(num) / self.sample_freq
1129
+ mpos = self.mics.pos[:, :, np.newaxis]
1130
+ t = self.start + np.ones(num_mics)[:, np.newaxis] * np.arange(num) / self.sample_freq
1172
1131
  epslim = 0.1 / self.up / self.sample_freq
1173
1132
  c0 = self.env.c
1174
1133
  tr = self.trajectory
1175
1134
  n = self.num_samples
1176
1135
  while n > 0:
1177
- eps = ones_like(t) # init discrepancy in time
1136
+ eps = np.ones_like(t) # init discrepancy in time
1178
1137
  te = t.copy() # init emission time = receiving time
1179
1138
  j = 0
1180
1139
  # Newton-Rhapson iteration
1181
1140
  while abs(eps).max() > epslim and j < 100:
1182
- loc = array(tr.location(te.flatten())).reshape((3, num_mics, -1))
1183
- rm = loc - mpos # distance vectors to microphones
1184
- rm = sqrt((rm * rm).sum(0)) # absolute distance
1185
- loc /= sqrt((loc * loc).sum(0)) # distance unit vector
1186
- der = array(tr.location(te.flatten(), der=1)).reshape((3, num_mics, -1))
1187
- Mr = (der * loc).sum(0) / c0 # radial Mach number
1188
- eps[:] = (te + rm / c0 - t) / (1 + Mr) # discrepancy in time
1141
+ loc = np.array(tr.location(te.flatten())).reshape((3, num_mics, -1))
1142
+ der = np.array(tr.location(te.flatten(), der=1)).reshape((3, num_mics, -1))
1143
+ dv = mpos - loc # distance vectors from source to microphones
1144
+ rm = np.sqrt((dv * dv).sum(0)) # absolute distance
1145
+ dv /= rm # just directions from source to microphones
1146
+ Mr = (der * dv).sum(0) / c0 # radial Mach number
1147
+ eps[:] = (te + rm / c0 - t) / (1 - Mr) # discrepancy in time
1189
1148
  te -= eps
1190
1149
  j += 1 # iteration count
1191
1150
  t += num / self.sample_freq
@@ -1194,7 +1153,7 @@ class MovingPointSource(PointSource):
1194
1153
  if self.conv_amp:
1195
1154
  rm *= (1 - Mr) ** 2
1196
1155
  try:
1197
- ind = array(0.5 + ind * self.up, dtype=int64)
1156
+ ind = np.array(0.5 + ind * self.up, dtype=np.int64)
1198
1157
  out = (signal[ind] / rm).T
1199
1158
  yield out[:n]
1200
1159
  except IndexError: # last incomplete frame
@@ -1221,7 +1180,7 @@ class PointSourceDipole(PointSource):
1221
1180
 
1222
1181
  See Also
1223
1182
  --------
1224
- :class:`acoular.sources.PointSource` : For modeling stationary point sources.
1183
+ :class:`~acoular.sources.PointSource` : For modeling stationary point sources.
1225
1184
 
1226
1185
  Notes
1227
1186
  -----
@@ -1239,11 +1198,11 @@ class PointSourceDipole(PointSource):
1239
1198
  #: Default is ``(0.0, 0.0, 1.0)`` (z-axis orientation).
1240
1199
  #:
1241
1200
  #: **Note:** Use vectors with order of magnitude around ``1.0`` or less for good results.
1242
- direction = Tuple((0.0, 0.0, 1.0), desc='dipole orientation and distance of the inversely phased monopoles')
1201
+ direction = Tuple((0.0, 0.0, 1.0))
1243
1202
 
1244
1203
  #: Behavior of the signal for negative time indices. Currently only supports ``'loop'``.
1245
1204
  #: Default is ``'loop'``.
1246
- prepadding = Enum('loop', desc='Behaviour for negative time indices.')
1205
+ prepadding = Enum('loop')
1247
1206
 
1248
1207
  #: A unique identifier for the current state of the source, based on its properties. (read-only)
1249
1208
  digest = Property(
@@ -1296,10 +1255,10 @@ class PointSourceDipole(PointSource):
1296
1255
 
1297
1256
  mpos = self.mics.pos
1298
1257
  # position of the dipole as (3,1) vector
1299
- loc = array(self.loc, dtype=float).reshape((3, 1))
1258
+ loc = self.loc
1300
1259
  # direction vector from tuple
1301
- direc = array(self.direction, dtype=float) * 1e-5
1302
- direc_mag = sqrt(dot(direc, direc))
1260
+ direc = np.array(self.direction, dtype=float) * 1e-5
1261
+ direc_mag = np.sqrt(np.dot(direc, direc))
1303
1262
 
1304
1263
  # normed direction vector
1305
1264
  direc_n = direc / direc_mag
@@ -1313,7 +1272,7 @@ class PointSourceDipole(PointSource):
1313
1272
  dir2 = (direc_n * dist / 2.0).reshape((3, 1))
1314
1273
 
1315
1274
  signal = self.signal.usignal(self.up)
1316
- out = empty((num, self.num_channels))
1275
+ out = np.empty((num, self.num_channels))
1317
1276
 
1318
1277
  # distance from dipole center to microphones
1319
1278
  rm = self.env._r(loc, mpos)
@@ -1336,8 +1295,8 @@ class PointSourceDipole(PointSource):
1336
1295
  rm
1337
1296
  / dist
1338
1297
  * (
1339
- signal[array(0.5 + ind1 * self.up, dtype=int64)] / rm1
1340
- - signal[array(0.5 + ind2 * self.up, dtype=int64)] / rm2
1298
+ signal[np.array(0.5 + ind1 * self.up, dtype=np.int64)] / rm1
1299
+ - signal[np.array(0.5 + ind2 * self.up, dtype=np.int64)] / rm2
1341
1300
  )
1342
1301
  )
1343
1302
  ind1 += 1.0
@@ -1350,7 +1309,8 @@ class PointSourceDipole(PointSource):
1350
1309
  except IndexError:
1351
1310
  break
1352
1311
 
1353
- yield out[:i]
1312
+ if i > 0:
1313
+ yield out[:i]
1354
1314
 
1355
1315
 
1356
1316
  class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
@@ -1370,8 +1330,8 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
1370
1330
 
1371
1331
  See Also
1372
1332
  --------
1373
- :class:`acoular.sources.PointSourceDipole` : For stationary dipole sources.
1374
- :class:`acoular.sources.MovingPointSource` :
1333
+ :class:`~acoular.sources.PointSourceDipole` : For stationary dipole sources.
1334
+ :class:`~acoular.sources.MovingPointSource` :
1375
1335
  For moving point sources without dipole characteristics.
1376
1336
  """
1377
1337
 
@@ -1392,7 +1352,7 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
1392
1352
  #: A reference vector, perpendicular to the x and y-axis of moving source, defining the axis of
1393
1353
  #: rotation for the dipole directivity. If set to ``(0, 0, 0)``, the dipole is only translated
1394
1354
  #: along the :attr:`~MovingPointSource.trajectory` without rotation. Default is ``(0, 0, 0)``.
1395
- rvec = CArray(dtype=float, shape=(3,), value=array((0, 0, 0)), desc='reference vector')
1355
+ rvec = CArray(dtype=float, shape=(3,), value=np.array((0, 0, 0)))
1396
1356
 
1397
1357
  @cached_property
1398
1358
  def _get_digest(self):
@@ -1434,70 +1394,25 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
1434
1394
  terminates when the time discrepancy (``eps``) is below a threshold (``epslim``)
1435
1395
  or after 100 iterations.
1436
1396
  """
1437
- eps = ones(self.mics.num_mics)
1397
+ eps = np.ones(self.mics.num_mics)
1438
1398
  epslim = 0.1 / self.up / self.sample_freq
1439
1399
  te = t.copy() # init emission time = receiving time
1440
1400
  j = 0
1441
1401
  # Newton-Rhapson iteration
1442
1402
  while abs(eps).max() > epslim and j < 100:
1443
- xs = array(self.trajectory.location(te))
1403
+ xs = np.array(self.trajectory.location(te))
1444
1404
  loc = xs.copy()
1445
1405
  loc += direction
1446
1406
  rm = loc - self.mics.pos # distance vectors to microphones
1447
- rm = sqrt((rm * rm).sum(0)) # absolute distance
1448
- loc /= sqrt((loc * loc).sum(0)) # distance unit vector
1449
- der = array(self.trajectory.location(te, der=1))
1407
+ rm = np.sqrt((rm * rm).sum(0)) # absolute distance
1408
+ loc /= np.sqrt((loc * loc).sum(0)) # distance unit vector
1409
+ der = np.array(self.trajectory.location(te, der=1))
1450
1410
  Mr = (der * loc).sum(0) / self.env.c # radial Mach number
1451
1411
  eps = (te + rm / self.env.c - t) / (1 + Mr) # discrepancy in time
1452
1412
  te -= eps
1453
1413
  j += 1 # iteration count
1454
1414
  return te, rm, Mr, xs
1455
1415
 
1456
- def get_moving_direction(self, direction, time=0):
1457
- """
1458
- Calculate the moving direction of the dipole source along its trajectory.
1459
-
1460
- This method computes the updated direction vector for the dipole source, considering both
1461
- translation along the trajectory and rotation defined by the :attr:`reference vector<rvec>`.
1462
- If the reference vector is ``(0, 0, 0)``, only translation is applied. Otherwise, the method
1463
- incorporates rotation into the calculation.
1464
-
1465
- Parameters
1466
- ----------
1467
- direction : :class:`numpy.ndarray`
1468
- The initial direction vector of the dipole, specified as a 3-element
1469
- array representing the orientation of the dipole lobes.
1470
- time : :class:`float`, optional
1471
- The time at which the trajectory position and velocity are evaluated. Defaults to ``0``.
1472
-
1473
- Returns
1474
- -------
1475
- :class:`numpy.ndarray`
1476
- The updated direction vector of the dipole source after translation
1477
- and, if applicable, rotation. The output is a 3-element array.
1478
-
1479
- Notes
1480
- -----
1481
- - The method computes the translation direction vector based on the trajectory's velocity at
1482
- the specified time.
1483
- - If the :attr:`reference vector<rvec>` is non-zero, the method constructs a rotation matrix
1484
- to compute the new dipole direction based on the trajectory's motion and the
1485
- reference vector.
1486
- - The rotation matrix ensures that the new dipole orientation adheres
1487
- to the right-hand rule and remains orthogonal.
1488
- """
1489
- trajg1 = array(self.trajectory.location(time, der=1))[:, 0][:, newaxis]
1490
- rflag = (self.rvec == 0).all() # flag translation vs. rotation
1491
- if rflag:
1492
- return direction
1493
- dx = array(trajg1.T) # direction vector (new x-axis)
1494
- dy = cross(self.rvec, dx) # new y-axis
1495
- dz = cross(dx, dy) # new z-axis
1496
- RM = array((dx, dy, dz)).T # rotation matrix
1497
- RM /= sqrt((RM * RM).sum(0)) # column normalized
1498
- newdir = dot(RM, direction)
1499
- return cross(newdir[:, 0].T, self.rvec.T).T
1500
-
1501
1416
  def result(self, num=128):
1502
1417
  """
1503
1418
  Generate the output signal at microphones in blocks.
@@ -1524,8 +1439,8 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
1524
1439
  mpos = self.mics.pos
1525
1440
 
1526
1441
  # direction vector from tuple
1527
- direc = array(self.direction, dtype=float) * 1e-5
1528
- direc_mag = sqrt(dot(direc, direc))
1442
+ direc = np.array(self.direction, dtype=float) * 1e-5
1443
+ direc_mag = np.sqrt(np.dot(direc, direc))
1529
1444
  # normed direction vector
1530
1445
  direc_n = direc / direc_mag
1531
1446
  c = self.env.c
@@ -1536,10 +1451,10 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
1536
1451
  dir2 = (direc_n * dist / 2.0).reshape((3, 1))
1537
1452
 
1538
1453
  signal = self.signal.usignal(self.up)
1539
- out = empty((num, self.num_channels))
1454
+ out = np.empty((num, self.num_channels))
1540
1455
  # shortcuts and initial values
1541
1456
  m = self.mics
1542
- t = self.start * ones(m.num_mics)
1457
+ t = self.start * np.ones(m.num_mics)
1543
1458
 
1544
1459
  i = 0
1545
1460
  n = self.num_samples
@@ -1548,7 +1463,7 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
1548
1463
  te, rm, Mr, locs = self.get_emission_time(t, 0)
1549
1464
  t += 1.0 / self.sample_freq
1550
1465
  # location of the center
1551
- loc = array(self.trajectory.location(te), dtype=float)[:, 0][:, newaxis]
1466
+ loc = np.array(self.trajectory.location(te), dtype=float)[:, 0][:, np.newaxis]
1552
1467
  # distance of the dipoles from the center
1553
1468
  diff = self.get_moving_direction(dir2, te)
1554
1469
 
@@ -1567,8 +1482,8 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
1567
1482
  rm
1568
1483
  / dist
1569
1484
  * (
1570
- signal[array(0.5 + ind * self.up, dtype=int64)] / rm1
1571
- - signal[array(0.5 + ind * self.up, dtype=int64)] / rm2
1485
+ signal[np.array(0.5 + ind * self.up, dtype=np.int64)] / rm1
1486
+ - signal[np.array(0.5 + ind * self.up, dtype=np.int64)] / rm2
1572
1487
  )
1573
1488
  )
1574
1489
  i += 1
@@ -1577,7 +1492,9 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
1577
1492
  i = 0
1578
1493
  except IndexError:
1579
1494
  break
1580
- yield out[:i]
1495
+
1496
+ if i > 0:
1497
+ yield out[:i]
1581
1498
 
1582
1499
 
1583
1500
  class LineSource(PointSource):
@@ -1598,7 +1515,7 @@ class LineSource(PointSource):
1598
1515
 
1599
1516
  See Also
1600
1517
  --------
1601
- :class:`acoular.sources.PointSource` : For modeling stationary point sources.
1518
+ :class:`~acoular.sources.PointSource` : For modeling stationary point sources.
1602
1519
 
1603
1520
  Notes
1604
1521
  -----
@@ -1606,19 +1523,19 @@ class LineSource(PointSource):
1606
1523
  """
1607
1524
 
1608
1525
  #: Vector to define the orientation of the line source. Default is ``(0.0, 0.0, 1.0)``.
1609
- direction = Tuple((0.0, 0.0, 1.0), desc='Line orientation ')
1526
+ direction = Tuple((0.0, 0.0, 1.0))
1610
1527
 
1611
1528
  #: Vector to define the length of the line source in meters. Default is ``1.0``.
1612
- length = Float(1, desc='length of the line source')
1529
+ length = Float(1)
1613
1530
 
1614
1531
  #: Number of monopole sources in the line source. Default is ``1``.
1615
1532
  num_sources = Int(1)
1616
1533
 
1617
1534
  #: Strength coefficients for each monopole source.
1618
- source_strength = CArray(desc='coefficients of the source strength')
1535
+ source_strength = CArray()
1619
1536
 
1620
1537
  #: Coherence mode for the monopoles (``'coherent'`` or ``'incoherent'``).
1621
- coherence = Enum('coherent', 'incoherent', desc='coherence mode')
1538
+ coherence = Enum('coherent', 'incoherent')
1622
1539
 
1623
1540
  #: A unique identifier for the current state of the source, based on its properties. (read-only)
1624
1541
  digest = Property(
@@ -1662,24 +1579,24 @@ class LineSource(PointSource):
1662
1579
  mpos = self.mics.pos
1663
1580
 
1664
1581
  # direction vector from tuple
1665
- direc = array(self.direction, dtype=float)
1582
+ direc = np.array(self.direction, dtype=float)
1666
1583
  # normed direction vector
1667
- direc_n = direc / norm(direc)
1584
+ direc_n = direc / spla.norm(direc)
1668
1585
  c = self.env.c
1669
1586
 
1670
1587
  # distance between monopoles in the line
1671
1588
  dist = self.length / self.num_sources
1672
1589
 
1673
1590
  # blocwise output
1674
- out = zeros((num, self.num_channels))
1591
+ out = np.zeros((num, self.num_channels))
1675
1592
 
1676
1593
  # distance from line start position to microphones
1677
- loc = array(self.loc, dtype=float).reshape((3, 1))
1594
+ loc = self.loc
1678
1595
 
1679
1596
  # distances from monopoles in the line to microphones
1680
- rms = empty((self.num_channels, self.num_sources))
1681
- inds = empty((self.num_channels, self.num_sources))
1682
- signals = empty((self.num_sources, len(self.signal.usignal(self.up))))
1597
+ rms = np.empty((self.num_channels, self.num_sources))
1598
+ inds = np.empty((self.num_channels, self.num_sources))
1599
+ signals = np.empty((self.num_sources, len(self.signal.usignal(self.up))))
1683
1600
  # for every source - distances
1684
1601
  for s in range(self.num_sources):
1685
1602
  rms[:, s] = self.env._r((loc.T + direc_n * dist * s).T, mpos)
@@ -1696,18 +1613,19 @@ class LineSource(PointSource):
1696
1613
  try:
1697
1614
  for s in range(self.num_sources):
1698
1615
  # sum sources
1699
- out[i] += signals[s, array(0.5 + inds[:, s].T * self.up, dtype=int64)] / rms[:, s]
1616
+ out[i] += signals[s, np.array(0.5 + inds[:, s].T * self.up, dtype=np.int64)] / rms[:, s]
1700
1617
 
1701
1618
  inds += 1.0
1702
1619
  i += 1
1703
1620
  if i == num:
1704
1621
  yield out
1705
- out = zeros((num, self.num_channels))
1622
+ out = np.zeros((num, self.num_channels))
1706
1623
  i = 0
1707
1624
  except IndexError:
1708
1625
  break
1709
1626
 
1710
- yield out[:i]
1627
+ if i > 0:
1628
+ yield out[:i]
1711
1629
 
1712
1630
 
1713
1631
  class MovingLineSource(LineSource, MovingPointSource):
@@ -1727,10 +1645,10 @@ class MovingLineSource(LineSource, MovingPointSource):
1727
1645
 
1728
1646
  See Also
1729
1647
  --------
1730
- :class:`acoular.sources.LineSource` :
1648
+ :class:`~acoular.sources.LineSource` :
1731
1649
  For :class:`line sources<LineSource>` consisting of
1732
1650
  :attr:`coherent or incoherent<LineSource.coherence>` monopoles.
1733
- :class:`acoular.sources.MovingPointSource` :
1651
+ :class:`~acoular.sources.MovingPointSource` :
1734
1652
  For moving point sources without dipole characteristics.
1735
1653
  """
1736
1654
 
@@ -1752,59 +1670,12 @@ class MovingLineSource(LineSource, MovingPointSource):
1752
1670
  #: rotation for the line source directivity. If set to ``(0, 0, 0)``, the line source is only
1753
1671
  #: translated along the :attr:`~MovingPointSource.trajectory` without rotation. Default is
1754
1672
  #: ``(0, 0, 0)``.
1755
- rvec = CArray(dtype=float, shape=(3,), value=array((0, 0, 0)), desc='reference vector')
1673
+ rvec = CArray(dtype=float, shape=(3,), value=np.array((0, 0, 0)))
1756
1674
 
1757
1675
  @cached_property
1758
1676
  def _get_digest(self):
1759
1677
  return digest(self)
1760
1678
 
1761
- def get_moving_direction(self, direction, time=0):
1762
- """
1763
- Calculate the moving direction of the line source along its trajectory.
1764
-
1765
- This method computes the updated direction vector for the line source,
1766
- considering both translation along the :attr:`~MovingPointSource.trajectory` and rotation
1767
- defined by the :attr:`reference vector<rvec>`. If the :attr:`reference vector<rvec>` is
1768
- `(0, 0, 0)`, only translation is applied. Otherwise, the method incorporates rotation
1769
- into the calculation.
1770
-
1771
- Parameters
1772
- ----------
1773
- direction : :class:`numpy.ndarray`
1774
- The initial direction vector of the line source, specified as a
1775
- 3-element array representing the orientation of the line.
1776
- time : :class:`float`, optional
1777
- The time at which the :attr:`~MovingPointSource.trajectory` position and velocity
1778
- are evaluated. Defaults to ``0``.
1779
-
1780
- Returns
1781
- -------
1782
- :class:`numpy.ndarray`
1783
- The updated direction vector of the line source after translation and,
1784
- if applicable, rotation. The output is a 3-element array.
1785
-
1786
- Notes
1787
- -----
1788
- - The method computes the translation direction vector based on the
1789
- :attr:`~MovingPointSource.trajectory`'s velocity at the specified time.
1790
- - If the :attr:`reference vector<rvec>` is non-zero, the method constructs a
1791
- rotation matrix to compute the new line source direction based on the
1792
- :attr:`~MovingPointSource.trajectory`'s motion and the :attr:`reference vector<rvec>`.
1793
- - The rotation matrix ensures that the new orientation adheres to the
1794
- right-hand rule and remains orthogonal.
1795
- """
1796
- trajg1 = array(self.trajectory.location(time, der=1))[:, 0][:, newaxis]
1797
- rflag = (self.rvec == 0).all() # flag translation vs. rotation
1798
- if rflag:
1799
- return direction
1800
- dx = array(trajg1.T) # direction vector (new x-axis)
1801
- dy = cross(self.rvec, dx) # new y-axis
1802
- dz = cross(dx, dy) # new z-axis
1803
- RM = array((dx, dy, dz)).T # rotation matrix
1804
- RM /= sqrt((RM * RM).sum(0)) # column normalized
1805
- newdir = dot(RM, direction)
1806
- return cross(newdir[:, 0].T, self.rvec.T).T
1807
-
1808
1679
  def get_emission_time(self, t, direction):
1809
1680
  """
1810
1681
  Calculate the emission time for a moving line source based on its trajectory.
@@ -1847,19 +1718,19 @@ class MovingLineSource(LineSource, MovingPointSource):
1847
1718
  - The method iterates until the difference between the computed emission time and
1848
1719
  the current time is sufficiently small (within a defined threshold).
1849
1720
  """
1850
- eps = ones(self.mics.num_mics)
1721
+ eps = np.ones(self.mics.num_mics)
1851
1722
  epslim = 0.1 / self.up / self.sample_freq
1852
1723
  te = t.copy() # init emission time = receiving time
1853
1724
  j = 0
1854
1725
  # Newton-Rhapson iteration
1855
1726
  while abs(eps).max() > epslim and j < 100:
1856
- xs = array(self.trajectory.location(te))
1727
+ xs = np.array(self.trajectory.location(te))
1857
1728
  loc = xs.copy()
1858
1729
  loc += direction
1859
1730
  rm = loc - self.mics.pos # distance vectors to microphones
1860
- rm = sqrt((rm * rm).sum(0)) # absolute distance
1861
- loc /= sqrt((loc * loc).sum(0)) # distance unit vector
1862
- der = array(self.trajectory.location(te, der=1))
1731
+ rm = np.sqrt((rm * rm).sum(0)) # absolute distance
1732
+ loc /= np.sqrt((loc * loc).sum(0)) # distance unit vector
1733
+ der = np.array(self.trajectory.location(te, der=1))
1863
1734
  Mr = (der * loc).sum(0) / self.env.c # radial Mach number
1864
1735
  eps = (te + rm / self.env.c - t) / (1 + Mr) # discrepancy in time
1865
1736
  te -= eps
@@ -1887,21 +1758,21 @@ class MovingLineSource(LineSource, MovingPointSource):
1887
1758
  mpos = self.mics.pos
1888
1759
 
1889
1760
  # direction vector from tuple
1890
- direc = array(self.direction, dtype=float)
1761
+ direc = np.array(self.direction, dtype=float)
1891
1762
  # normed direction vector
1892
- direc_n = direc / norm(direc)
1763
+ direc_n = direc / spla.norm(direc)
1893
1764
 
1894
1765
  # distance between monopoles in the line
1895
1766
  dist = self.length / self.num_sources
1896
1767
  dir2 = (direc_n * dist).reshape((3, 1))
1897
1768
 
1898
1769
  # blocwise output
1899
- out = zeros((num, self.num_channels))
1770
+ out = np.zeros((num, self.num_channels))
1900
1771
 
1901
1772
  # distances from monopoles in the line to microphones
1902
- rms = empty((self.num_channels, self.num_sources))
1903
- inds = empty((self.num_channels, self.num_sources))
1904
- signals = empty((self.num_sources, len(self.signal.usignal(self.up))))
1773
+ rms = np.empty((self.num_channels, self.num_sources))
1774
+ inds = np.empty((self.num_channels, self.num_sources))
1775
+ signals = np.empty((self.num_sources, len(self.signal.usignal(self.up))))
1905
1776
  # coherence
1906
1777
  for s in range(self.num_sources):
1907
1778
  # new seed for every source
@@ -1913,20 +1784,20 @@ class MovingLineSource(LineSource, MovingPointSource):
1913
1784
 
1914
1785
  # shortcuts and initial values
1915
1786
  m = self.mics
1916
- t = self.start * ones(m.num_mics)
1787
+ t = self.start * np.ones(m.num_mics)
1917
1788
  i = 0
1918
1789
  n = self.num_samples
1919
1790
  while n:
1920
1791
  n -= 1
1921
1792
  t += 1.0 / self.sample_freq
1922
1793
  te1, rm1, Mr1, locs1 = self.get_emission_time(t, 0)
1923
- # trajg1 = array(self.trajectory.location( te1, der=1))[:,0][:,newaxis]
1794
+ # trajg1 = np.array(self.trajectory.location( te1, der=1))[:,0][:,np.newaxis]
1924
1795
 
1925
1796
  # get distance and ind for every source in the line
1926
1797
  for s in range(self.num_sources):
1927
1798
  diff = self.get_moving_direction(dir2, te1)
1928
- te, rm, Mr, locs = self.get_emission_time(t, tile((diff * s).T, (self.num_channels, 1)).T)
1929
- loc = array(self.trajectory.location(te), dtype=float)[:, 0][:, newaxis]
1799
+ te, rm, Mr, locs = self.get_emission_time(t, np.tile((diff * s).T, (self.num_channels, 1)).T)
1800
+ loc = np.array(self.trajectory.location(te), dtype=float)[:, 0][:, np.newaxis]
1930
1801
  diff = self.get_moving_direction(dir2, te)
1931
1802
  rms[:, s] = self.env._r((loc + diff * s), mpos)
1932
1803
  inds[:, s] = (te - self.start_t + self.start) * self.sample_freq
@@ -1938,19 +1809,20 @@ class MovingLineSource(LineSource, MovingPointSource):
1938
1809
  # subtract the second signal b/c of phase inversion
1939
1810
  for s in range(self.num_sources):
1940
1811
  # sum sources
1941
- out[i] += signals[s, array(0.5 + inds[:, s].T * self.up, dtype=int64)] / rms[:, s]
1812
+ out[i] += signals[s, np.array(0.5 + inds[:, s].T * self.up, dtype=np.int64)] / rms[:, s]
1942
1813
 
1943
1814
  i += 1
1944
1815
  if i == num:
1945
1816
  yield out
1946
- out = zeros((num, self.num_channels))
1817
+ out = np.zeros((num, self.num_channels))
1947
1818
  i = 0
1948
1819
  except IndexError:
1949
1820
  break
1950
- yield out[:i]
1821
+
1822
+ if i > 0:
1823
+ yield out[:i]
1951
1824
 
1952
1825
 
1953
- @deprecated_alias({'numchannels': 'num_channels'}, read_only=True, removal_version='25.10')
1954
1826
  class UncorrelatedNoiseSource(SamplesGenerator):
1955
1827
  """
1956
1828
  Simulate uncorrelated white or pink noise signals at multiple channels.
@@ -1962,8 +1834,8 @@ class UncorrelatedNoiseSource(SamplesGenerator):
1962
1834
 
1963
1835
  See Also
1964
1836
  --------
1965
- :class:`acoular.signals.SignalGenerator` : For defining noise types and properties.
1966
- :class:`acoular.microphones.MicGeom` : For specifying microphone geometries.
1837
+ :class:`~acoular.signals.SignalGenerator` : For defining noise types and properties.
1838
+ :class:`~acoular.microphones.MicGeom` : For specifying microphone geometries.
1967
1839
 
1968
1840
  Notes
1969
1841
  -----
@@ -2006,12 +1878,12 @@ class UncorrelatedNoiseSource(SamplesGenerator):
2006
1878
  #: Instance of a :class:`~acoular.signals.NoiseGenerator`-derived class. For example:
2007
1879
  #: - :class:`~acoular.signals.WNoiseGenerator` for white noise.
2008
1880
  #: - :class:`~acoular.signals.PNoiseGenerator` for pink noise.
2009
- signal = Instance(NoiseGenerator, desc='type of noise')
1881
+ signal = Instance(NoiseGenerator)
2010
1882
 
2011
1883
  #: Array of random seed values for generating uncorrelated noise at each channel. If left empty,
2012
1884
  #: seeds will be automatically generated as ``np.arange(self.num_channels) + signal.seed``. The
2013
1885
  #: size of the array must match the :attr:`number of output channels<num_channels>`.
2014
- seed = CArray(dtype=uint32, desc='random seed values')
1886
+ seed = CArray(dtype=np.uint32)
2015
1887
 
2016
1888
  #: Number of output channels, automatically determined by the number of microphones
2017
1889
  #: defined in the :attr:`mics` attribute. Corresponds to the number of uncorrelated noise
@@ -2021,15 +1893,15 @@ class UncorrelatedNoiseSource(SamplesGenerator):
2021
1893
  #: :class:`~acoular.microphones.MicGeom` object specifying the positions of microphones.
2022
1894
  #: This attribute is used to define the microphone geometry and the
2023
1895
  #: :attr:`number of channels<num_channels>`.
2024
- mics = Instance(MicGeom, desc='microphone geometry')
1896
+ mics = Instance(MicGeom)
2025
1897
 
2026
1898
  #: Start time of the generated noise signal in seconds. Determines the time offset for the noise
2027
1899
  #: output relative to the start of data acquisition. Default is ``0.0``.
2028
- start_t = Float(0.0, desc='signal start time')
1900
+ start_t = Float(0.0)
2029
1901
 
2030
1902
  #: Start time of data acquisition at the microphones in seconds. This value determines when the
2031
1903
  #: generated noise begins relative to the acquisition process. Default is ``0.0``.
2032
- start = Float(0.0, desc='sample start time')
1904
+ start = Float(0.0)
2033
1905
 
2034
1906
  #: Total number of samples in the noise signal, derived from the :attr:`signal` generator.
2035
1907
  #: This value determines the length of the output signal for all channels.
@@ -2089,14 +1961,14 @@ class UncorrelatedNoiseSource(SamplesGenerator):
2089
1961
  Noise = self.signal.__class__
2090
1962
  # create or get the array of random seeds
2091
1963
  if not self.seed.size > 0:
2092
- seed = arange(self.num_channels) + self.signal.seed
1964
+ seed = np.arange(self.num_channels) + self.signal.seed
2093
1965
  elif self.seed.shape == (self.num_channels,):
2094
1966
  seed = self.seed
2095
1967
  else:
2096
1968
  msg = f'Seed array expected to be of shape ({self.num_channels:d},), but has shape {self.seed.shape}.'
2097
1969
  raise ValueError(msg)
2098
1970
  # create array with [num_channels] noise signal tracks
2099
- signal = array(
1971
+ signal = np.array(
2100
1972
  [
2101
1973
  Noise(seed=s, num_samples=self.num_samples, sample_freq=self.sample_freq, rms=self.signal.rms).signal()
2102
1974
  for s in seed
@@ -2114,7 +1986,6 @@ class UncorrelatedNoiseSource(SamplesGenerator):
2114
1986
  return
2115
1987
 
2116
1988
 
2117
- @deprecated_alias({'numchannels': 'num_channels', 'numsamples': 'num_samples'}, read_only=True, removal_version='25.10')
2118
1989
  class SourceMixer(SamplesGenerator):
2119
1990
  """
2120
1991
  Combine signals from multiple sources by mixing their outputs.
@@ -2126,7 +1997,7 @@ class SourceMixer(SamplesGenerator):
2126
1997
 
2127
1998
  See Also
2128
1999
  --------
2129
- :class:`acoular.base.SamplesGenerator` : Base class for signal generators.
2000
+ :class:`~acoular.base.SamplesGenerator` : Base class for signal generators.
2130
2001
 
2131
2002
  Notes
2132
2003
  -----
@@ -2228,7 +2099,7 @@ class SourceMixer(SamplesGenerator):
2228
2099
  #: The size of the weights array must match the number of sources in :attr:`sources`.
2229
2100
  #: For example, with two sources, ``weights = [1.0, 0.5]`` would mix the first source at
2230
2101
  #: full amplitude and the second source at half amplitude.
2231
- weights = CArray(desc='channel weights')
2102
+ weights = CArray()
2232
2103
 
2233
2104
  #: Internal identifier for the combined state of all sources, used to track
2234
2105
  #: changes in the sources for reproducibility and caching.
@@ -2316,7 +2187,7 @@ class SourceMixer(SamplesGenerator):
2316
2187
  gens = [i.result(num) for i in self.sources[1:]]
2317
2188
  weights = self.weights.copy()
2318
2189
  if weights.size == 0:
2319
- weights = array([1.0 for j in range(len(self.sources))])
2190
+ weights = np.array([1.0 for j in range(len(self.sources))])
2320
2191
  assert weights.shape[0] == len(self.sources)
2321
2192
  for temp in self.sources[0].result(num):
2322
2193
  temp *= weights[0]
@@ -2345,7 +2216,7 @@ class PointSourceConvolve(PointSource):
2345
2216
  See Also
2346
2217
  --------
2347
2218
  :class:`PointSource` : Base class for point sources.
2348
- :class:`acoular.tprocess.TimeConvolve` : Class used for performing time-domain convolution.
2219
+ :class:`~acoular.tprocess.TimeConvolve` : Class used for performing time-domain convolution.
2349
2220
 
2350
2221
  Notes
2351
2222
  -----
@@ -2386,8 +2257,7 @@ class PointSourceConvolve(PointSource):
2386
2257
  (256, 4)
2387
2258
  (256, 4)
2388
2259
  (256, 4)
2389
- (256, 4)
2390
- (75, 4)
2260
+ (232, 4)
2391
2261
 
2392
2262
  The last block has fewer samples.
2393
2263
  """
@@ -2395,24 +2265,36 @@ class PointSourceConvolve(PointSource):
2395
2265
  #: Convolution kernel in the time domain.
2396
2266
  #: The array must either have one column (a single kernel applied to all channels)
2397
2267
  #: or match the number of output channels in its second dimension.
2398
- kernel = CArray(dtype=float, desc='Convolution kernel.')
2268
+ kernel = CArray(dtype=float)
2399
2269
 
2400
2270
  #: Start time of the signal in seconds. Default is ``0.0``.
2401
- start_t = Enum(0.0, desc='signal start time')
2271
+ start_t = Enum(0.0)
2402
2272
 
2403
2273
  #: Start time of the data acquisition the the microphones in seconds. Default is ``0.0``.
2404
- start = Enum(0.0, desc='sample start time')
2274
+ start = Enum(0.0)
2405
2275
 
2406
2276
  #: Behavior for negative time indices. Default is ``None``.
2407
- prepadding = Enum(None, desc='Behavior for negative time indices.')
2277
+ prepadding = Enum(None)
2408
2278
 
2409
2279
  #: Upsampling factor for internal use. Default is ``None``.
2410
- up = Enum(None, desc='upsampling factor')
2280
+ up = Enum(None)
2411
2281
 
2412
2282
  #: Unique identifier for the current state of the source,
2413
2283
  #: based on microphone geometry, input signal, source location, and kernel. (read-only)
2414
2284
  digest = Property(depends_on=['mics.digest', 'signal.digest', 'loc', 'kernel'])
2415
2285
 
2286
+ #: Controls whether to extend the output to include the full convolution result.
2287
+ #:
2288
+ #: - If ``False`` (default): Output length is :math:`\\max(L, M)`, where :math:`L` is the
2289
+ #: kernel length and :math:`M` is the signal length. This mode keeps the output length
2290
+ #: equal to the longest input (different from NumPy's ``mode='same'``, since it does not
2291
+ #: pad the output).
2292
+ #: - If ``True``: Output length is :math:`L + M - 1`, returning the full convolution at
2293
+ #: each overlap point (similar to NumPy's ``mode='full'``).
2294
+ #:
2295
+ #: Default is ``False``.
2296
+ extend_signal = Bool(False)
2297
+
2416
2298
  @cached_property
2417
2299
  def _get_digest(self):
2418
2300
  return digest(self)
@@ -2444,7 +2326,7 @@ class PointSourceConvolve(PointSource):
2444
2326
  - Convolution is performed using the :class:`~acoular.tprocess.TimeConvolve` class
2445
2327
  to ensure efficiency.
2446
2328
  """
2447
- data = repeat(self.signal.signal()[:, newaxis], self.mics.num_mics, axis=1)
2329
+ data = np.repeat(self.signal.signal()[:, np.newaxis], self.mics.num_mics, axis=1)
2448
2330
  source = TimeSamples(
2449
2331
  data=data,
2450
2332
  sample_freq=self.sample_freq,
@@ -2454,5 +2336,6 @@ class PointSourceConvolve(PointSource):
2454
2336
  time_convolve = TimeConvolve(
2455
2337
  source=source,
2456
2338
  kernel=self.kernel,
2339
+ extend_signal=self.extend_signal,
2457
2340
  )
2458
2341
  yield from time_convolve.result(num)