acoular 24.10__py3-none-any.whl → 25.3__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 +31 -18
- acoular/deprecation.py +85 -0
- acoular/environments.py +481 -229
- acoular/fastFuncs.py +90 -84
- acoular/fbeamform.py +203 -411
- acoular/fprocess.py +233 -123
- acoular/grids.py +793 -424
- acoular/h5cache.py +29 -40
- acoular/h5files.py +2 -6
- acoular/microphones.py +197 -74
- acoular/process.py +660 -149
- acoular/sdinput.py +23 -20
- acoular/signals.py +461 -159
- acoular/sources.py +1311 -489
- acoular/spectra.py +328 -352
- 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 +126 -53
- acoular/version.py +2 -2
- {acoular-24.10.dist-info → acoular-25.3.dist-info}/METADATA +39 -17
- acoular-25.3.dist-info/RECORD +56 -0
- {acoular-24.10.dist-info → acoular-25.3.dist-info}/WHEEL +1 -1
- acoular-24.10.dist-info/RECORD +0 -54
- {acoular-24.10.dist-info → acoular-25.3.dist-info}/licenses/AUTHORS.rst +0 -0
- {acoular-24.10.dist-info → acoular-25.3.dist-info}/licenses/LICENSE +0 -0
acoular/calib.py
CHANGED
|
@@ -10,65 +10,115 @@
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
# imports from other packages
|
|
13
|
-
|
|
13
|
+
import xml.dom.minidom
|
|
14
14
|
|
|
15
|
-
from numpy import array
|
|
16
|
-
from traits.api import CArray,
|
|
15
|
+
from numpy import array, newaxis
|
|
16
|
+
from traits.api import CArray, CInt, File, List, Property, cached_property, on_trait_change
|
|
17
|
+
|
|
18
|
+
import acoular as ac
|
|
19
|
+
|
|
20
|
+
from .base import InOut
|
|
17
21
|
|
|
18
22
|
# acoular imports
|
|
23
|
+
from .deprecation import deprecated_alias
|
|
19
24
|
from .internal import digest
|
|
20
25
|
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
@deprecated_alias({'from_file': 'file'})
|
|
28
|
+
class Calib(InOut):
|
|
29
|
+
"""Processing block for handling calibration data in `*.xml` or NumPy format.
|
|
30
|
+
|
|
31
|
+
This class implements the application of calibration factors to the data obtained from its
|
|
32
|
+
:attr:`source`. The calibrated data can be accessed (e.g. for use in a block chain) via the
|
|
33
|
+
:meth:`result` generator. Depending on the source type, calibration can be performed in the
|
|
34
|
+
time or frequency domain.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
Examples
|
|
38
|
+
--------
|
|
39
|
+
Consider calibrating a time signal by specifying the calibration factors in NumPy format.
|
|
40
|
+
Assume that the following white noise signal is in Volt and the sensitivity of the virtual
|
|
41
|
+
sensor is 1e-2 V/Pa. Then, the voltage signal can be converted to a calibrated sound
|
|
42
|
+
pressure signal by multiplying it with a calibration factor of 100 Pa/V.
|
|
43
|
+
|
|
44
|
+
>>> import acoular as ac
|
|
45
|
+
>>> import numpy as np
|
|
46
|
+
>>>
|
|
47
|
+
>>> signal = ac.WNoiseGenerator(sample_freq=51200,# doctest: +SKIP
|
|
48
|
+
>>> num_samples=51200,# doctest: +SKIP
|
|
49
|
+
>>> rms=0.01).signal()# doctest: +SKIP
|
|
50
|
+
>>> ts = ac.TimeSamples(data=signal[:, np.newaxis], sample_freq=51200) # doctest: +SKIP
|
|
51
|
+
>>> calib = ac.Calib(source=ts) # doctest: +SKIP
|
|
52
|
+
>>> calib.data = np.array([100]) # doctest: +SKIP
|
|
53
|
+
>>> print(next(calib.result(num=1))) # doctest: +SKIP
|
|
54
|
+
[[1.76405235]]
|
|
55
|
+
|
|
56
|
+
The calibrated data can then be further processed, e.g. by calculating the FFT of the
|
|
57
|
+
calibrated data.
|
|
58
|
+
|
|
59
|
+
>>> fft = ac.RFFT(source=calib, block_size=16) # doctest: +SKIP
|
|
60
|
+
>>> print(next(fft.result(num=1))) # doctest: +SKIP
|
|
61
|
+
[[10.63879909+0.j 3.25957562-1.57652611j -2.27342854-3.39108312j
|
|
62
|
+
0.07458428+0.49657939j 1.772696 +3.92233098j 3.19543248+0.17988554j
|
|
63
|
+
0.3379041 -3.93342331j 0.93949242+2.5328611j 2.97352574+0.j ]]
|
|
64
|
+
|
|
65
|
+
One could also apply the calibration after the FFT calculation.
|
|
66
|
+
|
|
67
|
+
>>> fft = ac.RFFT(source=ts, block_size=16) # doctest: +SKIP
|
|
68
|
+
>>> calib = ac.Calib(source=fft) # doctest: +SKIP
|
|
69
|
+
>>> calib.data = 100 * np.ones(ts.num_channels * fft.num_freqs) # doctest: +SKIP
|
|
70
|
+
>>> print(next(calib.result(num=1))) # doctest: +SKIP
|
|
71
|
+
[[10.63879909+0.j 3.25957562-1.57652611j -2.27342854-3.39108312j
|
|
72
|
+
0.07458428+0.49657939j 1.772696 +3.92233098j 3.19543248+0.17988554j
|
|
73
|
+
0.3379041 -3.93342331j 0.93949242+2.5328611j 2.97352574+0.j ]]
|
|
74
|
+
|
|
75
|
+
Deprecated and will be removed in Acoular 25.10:
|
|
25
76
|
This class serves as interface to load calibration data for the used
|
|
26
77
|
microphone array. The calibration factors are stored as [Pa/unit].
|
|
27
78
|
"""
|
|
28
79
|
|
|
29
80
|
#: Name of the .xml file to be imported.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
#: Basename of the .xml-file. Readonly / is set automatically.
|
|
33
|
-
basename = Property(depends_on='from_file', desc='basename of xml file')
|
|
81
|
+
file = File(filter=['*.xml'], exists=True, desc='name of the xml file to import')
|
|
34
82
|
|
|
35
83
|
#: Number of microphones in the calibration data,
|
|
36
|
-
#: is set automatically
|
|
37
|
-
num_mics =
|
|
84
|
+
#: is set automatically when read from file or when data is set.
|
|
85
|
+
num_mics = CInt(0, desc='number of microphones in the geometry')
|
|
38
86
|
|
|
39
87
|
#: Array of calibration factors,
|
|
40
|
-
#: is set automatically
|
|
88
|
+
#: is set automatically when read from file.
|
|
89
|
+
#: Can be set manually by specifying a NumPy array with shape (num_channels, ) if
|
|
90
|
+
#: :attr:`source` yields time domain signals. For frequency domain signals, the expected
|
|
91
|
+
#: shape is (num_channels * num_freqs).
|
|
41
92
|
data = CArray(desc='calibration data')
|
|
42
93
|
|
|
94
|
+
#: Channels that are to be treated as invalid.
|
|
95
|
+
invalid_channels = List(int, desc='list of invalid channels')
|
|
96
|
+
|
|
97
|
+
#: Channel mask to serve as an index for all valid channels, is set automatically.
|
|
98
|
+
channels = Property(depends_on=['invalid_channels', 'num_mics'], desc='channel mask')
|
|
99
|
+
|
|
43
100
|
# Internal identifier
|
|
44
|
-
digest = Property(depends_on=['
|
|
101
|
+
digest = Property(depends_on=['source.digest', 'data'])
|
|
102
|
+
|
|
103
|
+
@on_trait_change('data')
|
|
104
|
+
def set_num_mics(self):
|
|
105
|
+
self.num_mics = self.data.shape[0]
|
|
45
106
|
|
|
46
107
|
@cached_property
|
|
47
|
-
def
|
|
48
|
-
|
|
108
|
+
def _get_channels(self):
|
|
109
|
+
if len(self.invalid_channels) == 0:
|
|
110
|
+
return slice(0, None, None)
|
|
111
|
+
allr = [i for i in range(self.num_mics) if i not in self.invalid_channels]
|
|
112
|
+
return array(allr)
|
|
49
113
|
|
|
50
114
|
@cached_property
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
return ''
|
|
54
|
-
return path.splitext(path.basename(self.from_file))[0]
|
|
115
|
+
def _get_digest(self):
|
|
116
|
+
return digest(self)
|
|
55
117
|
|
|
56
|
-
@on_trait_change('
|
|
118
|
+
@on_trait_change('file')
|
|
57
119
|
def import_data(self):
|
|
58
120
|
"""Loads the calibration data from `*.xml` file ."""
|
|
59
|
-
|
|
60
|
-
# empty calibration
|
|
61
|
-
if self.basename == '':
|
|
62
|
-
self.data = None
|
|
63
|
-
self.num_mics = 0
|
|
64
|
-
# no file there
|
|
65
|
-
else:
|
|
66
|
-
self.data = array([1.0], 'd')
|
|
67
|
-
self.num_mics = 1
|
|
68
|
-
return
|
|
69
|
-
import xml.dom.minidom
|
|
70
|
-
|
|
71
|
-
doc = xml.dom.minidom.parse(self.from_file)
|
|
121
|
+
doc = xml.dom.minidom.parse(self.file)
|
|
72
122
|
names = []
|
|
73
123
|
data = []
|
|
74
124
|
for element in doc.getElementsByTagName('pos'):
|
|
@@ -76,3 +126,48 @@ class Calib(HasPrivateTraits):
|
|
|
76
126
|
data.append(float(element.getAttribute('factor')))
|
|
77
127
|
self.data = array(data, 'd')
|
|
78
128
|
self.num_mics = self.data.shape[0]
|
|
129
|
+
|
|
130
|
+
def __validate_data(self):
|
|
131
|
+
"""Validates the calibration data."""
|
|
132
|
+
if self.data is None:
|
|
133
|
+
msg = 'No calibration data available.'
|
|
134
|
+
raise ValueError(msg)
|
|
135
|
+
if self.source is None:
|
|
136
|
+
msg = 'No source data available.'
|
|
137
|
+
raise ValueError(msg)
|
|
138
|
+
tobj = self.source
|
|
139
|
+
while isinstance(tobj, ac.InOut):
|
|
140
|
+
tobj = tobj.source
|
|
141
|
+
if isinstance(tobj, ac.SamplesGenerator) and (self.data[self.channels].shape[0] != tobj.num_channels):
|
|
142
|
+
msg = f'calibration data shape {self.data[self.channels].shape[0]} does not match \
|
|
143
|
+
source data shape {tobj.num_channels}'
|
|
144
|
+
raise ValueError(msg)
|
|
145
|
+
if isinstance(tobj, ac.SpectraGenerator) and (
|
|
146
|
+
self.data[self.channels].shape[0] != tobj.num_channels * tobj.num_freqs
|
|
147
|
+
):
|
|
148
|
+
msg = f'calibration data shape {self.data[self.channels].shape[0]} does not match \
|
|
149
|
+
source data shape {tobj.num_channels * tobj.num_freqs}'
|
|
150
|
+
raise ValueError(msg)
|
|
151
|
+
|
|
152
|
+
def result(self, num):
|
|
153
|
+
"""Python generator that processes the source data and yields the time-signal block-wise.
|
|
154
|
+
|
|
155
|
+
This method needs to be implemented by the derived classes.
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
num : int
|
|
160
|
+
This parameter defines the size of the blocks to be yielded
|
|
161
|
+
(i.e. the number of samples per block)
|
|
162
|
+
|
|
163
|
+
Yields
|
|
164
|
+
------
|
|
165
|
+
numpy.ndarray
|
|
166
|
+
Two-dimensional output data block of shape (num, sourcechannels)
|
|
167
|
+
where sourcechannels is num_channels if the source data is in the time domain
|
|
168
|
+
or sourcechannels is num_channels*num_freqs if the source data is in the frequency
|
|
169
|
+
domain.
|
|
170
|
+
"""
|
|
171
|
+
self.__validate_data()
|
|
172
|
+
for block in self.source.result(num):
|
|
173
|
+
yield block * self.data[self.channels][newaxis]
|
acoular/configuration.py
CHANGED
|
@@ -15,6 +15,8 @@ import sys
|
|
|
15
15
|
from os import environ, mkdir, path
|
|
16
16
|
from warnings import warn
|
|
17
17
|
|
|
18
|
+
# WARNING: DO NOT ADD ANY IMPORTS HERE THAT MIGHT IMPORT NUMPY
|
|
19
|
+
|
|
18
20
|
# When numpy is using OpenBLAS then it runs with OPENBLAS_NUM_THREADS which may lead to
|
|
19
21
|
# overcommittment when called from within numba jitted function that run on
|
|
20
22
|
# NUMBA_NUM_THREADS. If overcommitted, things get extremely! slow. Therefore we make an
|
|
@@ -50,11 +52,11 @@ if 'numpy' in sys.modules:
|
|
|
50
52
|
stacklevel=2,
|
|
51
53
|
)
|
|
52
54
|
else:
|
|
53
|
-
# numpy is not loaded
|
|
55
|
+
# numpy is not loaded, make sure that OpenBLAS runs single threaded
|
|
54
56
|
environ['OPENBLAS_NUM_THREADS'] = '1'
|
|
55
57
|
|
|
56
58
|
# this loads numpy, so we have to defer loading until OpenBLAS check is done
|
|
57
|
-
from traits.api import Bool, Enum, HasStrictTraits,
|
|
59
|
+
from traits.api import Bool, Enum, HasStrictTraits, File, Property, cached_property # noqa: I001
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
class Config(HasStrictTraits):
|
|
@@ -90,11 +92,11 @@ class Config(HasStrictTraits):
|
|
|
90
92
|
#: * 'overwrite': Acoular classes replace existing cachefile content with new data.
|
|
91
93
|
global_caching = Property()
|
|
92
94
|
|
|
93
|
-
_global_caching =
|
|
95
|
+
_global_caching = Enum('individual', 'all', 'none', 'readonly', 'overwrite')
|
|
94
96
|
|
|
95
97
|
#: Flag that globally defines package used to read and write .h5 files
|
|
96
|
-
#: defaults to 'pytables'. It is also possible to set it to 'tables', which is an alias for
|
|
97
|
-
#:
|
|
98
|
+
#: defaults to 'pytables'. It is also possible to set it to 'tables', which is an alias for
|
|
99
|
+
#: 'pytables'. If pytables cannot be imported, 'h5py' is used.
|
|
98
100
|
h5library = Property()
|
|
99
101
|
|
|
100
102
|
_h5library = Enum('pytables', 'tables', 'h5py')
|
|
@@ -104,14 +106,14 @@ class Config(HasStrictTraits):
|
|
|
104
106
|
#: it will be created. :attr:`cache_dir` defaults to current session path.
|
|
105
107
|
cache_dir = Property()
|
|
106
108
|
|
|
107
|
-
_cache_dir =
|
|
109
|
+
_cache_dir = File()
|
|
108
110
|
|
|
109
111
|
#: Defines the working directory containing data files. Used only by
|
|
110
112
|
#: :class:`~acoular.tprocess.WriteH5` class.
|
|
111
113
|
#: Defaults to current session path.
|
|
112
114
|
td_dir = Property()
|
|
113
115
|
|
|
114
|
-
_td_dir =
|
|
116
|
+
_td_dir = File(path.curdir)
|
|
115
117
|
|
|
116
118
|
#: Boolean Flag that determines whether user has access to traitsui features.
|
|
117
119
|
#: Defaults to False.
|
|
@@ -232,8 +234,8 @@ by :attr:`h5library`:
|
|
|
232
234
|
Some Acoular classes support GUI elements for usage with tools from the TraitsUI package.
|
|
233
235
|
If desired, this package has to be installed manually, as it is not a prerequisite for
|
|
234
236
|
installing Acoular.
|
|
235
|
-
To enable the functionality, the flag attribute :attr:`use_traitsui` has to be set to True (default:
|
|
236
|
-
Note: this is independent from the GUI tools implemented in the spectAcoular package.
|
|
237
|
+
To enable the functionality, the flag attribute :attr:`use_traitsui` has to be set to True (default:
|
|
238
|
+
False). Note: this is independent from the GUI tools implemented in the spectAcoular package.
|
|
237
239
|
|
|
238
240
|
|
|
239
241
|
Example:
|
acoular/demo/__init__.py
CHANGED
acoular/demo/acoular_demo.py
CHANGED
|
@@ -32,6 +32,29 @@ Source Location RMS
|
|
|
32
32
|
"""
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
def create_three_sources(mg, h5savefile='three_sources.h5'):
|
|
36
|
+
"""Create three noise sources and return them as Mixer."""
|
|
37
|
+
import acoular as ac
|
|
38
|
+
|
|
39
|
+
# set up the parameters
|
|
40
|
+
|
|
41
|
+
sfreq = 51200
|
|
42
|
+
duration = 1
|
|
43
|
+
nsamples = duration * sfreq
|
|
44
|
+
|
|
45
|
+
n1 = ac.WNoiseGenerator(sample_freq=sfreq, num_samples=nsamples, seed=1)
|
|
46
|
+
n2 = ac.WNoiseGenerator(sample_freq=sfreq, num_samples=nsamples, seed=2, rms=0.7)
|
|
47
|
+
n3 = ac.WNoiseGenerator(sample_freq=sfreq, num_samples=nsamples, seed=3, rms=0.5)
|
|
48
|
+
p1 = ac.PointSource(signal=n1, mics=mg, loc=(-0.1, -0.1, -0.3))
|
|
49
|
+
p2 = ac.PointSource(signal=n2, mics=mg, loc=(0.15, 0, -0.3))
|
|
50
|
+
p3 = ac.PointSource(signal=n3, mics=mg, loc=(0, 0.1, -0.3))
|
|
51
|
+
pa = ac.Mixer(source=p1, sources=[p2, p3])
|
|
52
|
+
if h5savefile:
|
|
53
|
+
wh5 = ac.WriteH5(source=pa, file=h5savefile)
|
|
54
|
+
wh5.save()
|
|
55
|
+
return pa
|
|
56
|
+
|
|
57
|
+
|
|
35
58
|
def run():
|
|
36
59
|
"""Run the Acoular demo."""
|
|
37
60
|
from pathlib import Path
|
|
@@ -40,30 +63,20 @@ def run():
|
|
|
40
63
|
|
|
41
64
|
ac.config.global_caching = 'none'
|
|
42
65
|
|
|
43
|
-
# set up
|
|
44
|
-
|
|
45
|
-
duration = 1
|
|
46
|
-
nsamples = duration * sfreq
|
|
66
|
+
# set up microphone geometry
|
|
67
|
+
|
|
47
68
|
micgeofile = Path(ac.__file__).parent / 'xml' / 'array_64.xml'
|
|
48
|
-
|
|
69
|
+
mg = ac.MicGeom(file=micgeofile)
|
|
49
70
|
|
|
50
71
|
# generate test data, in real life this would come from an array measurement
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
n2 = ac.WNoiseGenerator(sample_freq=sfreq, numsamples=nsamples, seed=2, rms=0.7)
|
|
54
|
-
n3 = ac.WNoiseGenerator(sample_freq=sfreq, numsamples=nsamples, seed=3, rms=0.5)
|
|
55
|
-
p1 = ac.PointSource(signal=n1, mics=mg, loc=(-0.1, -0.1, 0.3))
|
|
56
|
-
p2 = ac.PointSource(signal=n2, mics=mg, loc=(0.15, 0, 0.3))
|
|
57
|
-
p3 = ac.PointSource(signal=n3, mics=mg, loc=(0, 0.1, 0.3))
|
|
58
|
-
pa = ac.Mixer(source=p1, sources=[p2, p3])
|
|
59
|
-
wh5 = ac.WriteH5(source=pa, name=h5savefile)
|
|
60
|
-
wh5.save()
|
|
72
|
+
|
|
73
|
+
pa = create_three_sources(mg)
|
|
61
74
|
|
|
62
75
|
# analyze the data and generate map
|
|
63
76
|
|
|
64
77
|
ps = ac.PowerSpectra(source=pa, block_size=128, window='Hanning')
|
|
65
78
|
|
|
66
|
-
rg = ac.RectGrid(x_min=-0.2, x_max=0.2, y_min=-0.2, y_max=0.2, z
|
|
79
|
+
rg = ac.RectGrid(x_min=-0.2, x_max=0.2, y_min=-0.2, y_max=0.2, z=-0.3, increment=0.01)
|
|
67
80
|
st = ac.SteeringVector(grid=rg, mics=mg)
|
|
68
81
|
|
|
69
82
|
bb = ac.BeamformerBase(freq_data=ps, steer=st)
|
|
@@ -71,7 +84,7 @@ def run():
|
|
|
71
84
|
spl = ac.L_p(pm)
|
|
72
85
|
|
|
73
86
|
if ac.config.have_matplotlib:
|
|
74
|
-
from
|
|
87
|
+
from matplotlib.pyplot import axis, colorbar, figure, imshow, plot, show
|
|
75
88
|
|
|
76
89
|
# show map
|
|
77
90
|
imshow(spl.T, origin='lower', vmin=spl.max() - 10, extent=rg.extend(), interpolation='bicubic')
|
|
@@ -79,7 +92,7 @@ def run():
|
|
|
79
92
|
|
|
80
93
|
# plot microphone geometry
|
|
81
94
|
figure(2)
|
|
82
|
-
plot(mg.
|
|
95
|
+
plot(mg.pos[0], mg.pos[1], 'o')
|
|
83
96
|
axis('equal')
|
|
84
97
|
|
|
85
98
|
show()
|
acoular/deprecation.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# ------------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) Acoular Development Team.
|
|
3
|
+
# ------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
from warnings import warn
|
|
6
|
+
|
|
7
|
+
from traits.api import Property
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def deprecated_alias(old2new, read_only=False, removal_version=''):
|
|
11
|
+
"""Decorator function for deprecating renamed class traits.
|
|
12
|
+
Replaced traits should no longer be part of the class definition
|
|
13
|
+
and only mentioned in this decorator's parameter list.
|
|
14
|
+
The replacement trait has to be defined in the updated class and
|
|
15
|
+
will be mapped to the deprecated name via this function.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
old2new: dict
|
|
20
|
+
Dictionary containing the deprecated trait names as keys and
|
|
21
|
+
their new names as values.
|
|
22
|
+
read_only: bool or list
|
|
23
|
+
If True, all deprecated traits will be "read only".
|
|
24
|
+
If False (default), all deprecated traits can be read and set.
|
|
25
|
+
If list, traits whose names are in list will be "read only".
|
|
26
|
+
removal_version: string or dict
|
|
27
|
+
Adds the acoular version of trait removal to the deprecation message.
|
|
28
|
+
If a non-empty string, it will be interpreted as the acoular version when
|
|
29
|
+
all traits in the list will be deprecated.
|
|
30
|
+
If a dictionary, the keys are expected to be trait names and the values
|
|
31
|
+
are the removal version as strings.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def decorator(cls):
|
|
35
|
+
"""Decorator function that gets applied to the class `cls`."""
|
|
36
|
+
|
|
37
|
+
def _alias_accessor_factory(old, new, trait_read_only=False, trait_removal_version=''):
|
|
38
|
+
"""Function to define setter and getter routines for alias property trait."""
|
|
39
|
+
if trait_removal_version:
|
|
40
|
+
trait_removal_version = f' (will be removed in version {trait_removal_version})'
|
|
41
|
+
msg = f"Deprecated use of '{old}' trait{trait_removal_version}. Please use the '{new}' trait instead."
|
|
42
|
+
|
|
43
|
+
def getter(cls):
|
|
44
|
+
warn(msg, DeprecationWarning, stacklevel=2)
|
|
45
|
+
return getattr(cls, new)
|
|
46
|
+
|
|
47
|
+
if trait_read_only:
|
|
48
|
+
return (getter,)
|
|
49
|
+
|
|
50
|
+
def setter(cls, value):
|
|
51
|
+
warn(msg, DeprecationWarning, stacklevel=2)
|
|
52
|
+
setattr(cls, new, value)
|
|
53
|
+
|
|
54
|
+
return (getter, setter)
|
|
55
|
+
|
|
56
|
+
# Add deprecated traits to class traits and link them to
|
|
57
|
+
# the new ones with a deprecation warning.
|
|
58
|
+
for old, new in old2new.items():
|
|
59
|
+
# Set "read only" status depending on global read_only argument
|
|
60
|
+
current_read_only = (old in read_only) if isinstance(read_only, list) else read_only
|
|
61
|
+
# If version for trait removal is given, pass info to accessors
|
|
62
|
+
if isinstance(removal_version, str) and (len(removal_version) > 0):
|
|
63
|
+
current_removal_version = removal_version
|
|
64
|
+
elif isinstance(removal_version, dict) and (old in removal_version):
|
|
65
|
+
current_removal_version = removal_version[old]
|
|
66
|
+
else:
|
|
67
|
+
current_removal_version = ''
|
|
68
|
+
# Define Trait Property type
|
|
69
|
+
trait_type = Property(*_alias_accessor_factory(old, new, current_read_only, current_removal_version))
|
|
70
|
+
|
|
71
|
+
# If the new trait exists, set or update alias
|
|
72
|
+
if new in cls.class_traits():
|
|
73
|
+
if old not in cls.class_traits():
|
|
74
|
+
cls.add_class_trait(old, trait_type)
|
|
75
|
+
else:
|
|
76
|
+
# Access class dictionary to change trait
|
|
77
|
+
cls.__dict__['__class_traits__'][old] = trait_type
|
|
78
|
+
|
|
79
|
+
else:
|
|
80
|
+
error_msg = f"Cannot create trait '{old}' because its replacement trait '{new}' does not exist."
|
|
81
|
+
raise ValueError(error_msg)
|
|
82
|
+
|
|
83
|
+
return cls
|
|
84
|
+
|
|
85
|
+
return decorator
|