-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Expand file tree
/
Copy pathfilter_analysis.py
More file actions
226 lines (182 loc) · 7.15 KB
/
filter_analysis.py
File metadata and controls
226 lines (182 loc) · 7.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
"""Implements utils to model and combine filter properties, i.e. compute how
window size, stride, etc. behave, which may be useful for certain usecases such
as streaming.
Authors:
* Sylvain de Langen 2024
"""
from dataclasses import dataclass
@dataclass
class FilterProperties:
"""Models the properties of something that behaves like a filter (e.g.
convolutions, fbanks, etc.) over time.
"""
window_size: int
"""Size of the filter, i.e. the number of input frames on which a single
output depends. Other than dilation, it is assumed that the window operates
over a contiguous chunk of frames.
Example:
--------
.. code-block:: text
size = 3, stride = 3
out <-a-> <-b-> <-c->
in 1 2 3 4 5 6 7 8 9
"""
stride: int = 1
"""Stride of the filter, i.e. how many input frames get skipped over from an
output frame to the next (regardless of window size or dilation).
Example:
--------
.. code-block:: text
size = 3, stride = 2
<-a->
<-b-> <-d->
out <-c->
in 1 2 3 4 5 6 7 8 9
"""
dilation: int = 1
"""Dilation rate of the filter. A window will consider every n-th
(n=dilation) input frame. With dilation, the filter will still observe
`size` input frames, but the window will span more frames.
Dilation is mostly relevant to "a trous" convolutions.
A dilation rate of 1, the default, effectively performs no dilation.
Example:
--------
.. code-block:: text
size = 3, stride = 1, dilation = 3
<-------> dilation - 1 == 2 skips
a a a
| b | b | b
| | c | | c | | c
| | | d | | d | | d
| | | | e | | e | | ..
in 1 2 3 4 5 6 7 8 9 10 ..
<-> stride == 1
"""
causal: bool = False
"""Whether the filter is causal, i.e. whether an output frame only depends
on past input frames (of a lower or equal index).
In certain cases, such as 1D convolutions, this can simply be achieved by
inserting padding to the left of the filter prior to applying the filter to
the input tensor.
Example:
--------
.. code-block:: text
size = 3, stride = 1, causal = true
<-e->
<-d->
<-c->
b->
a
in 1 2 3 4 5
"""
def __post_init__(self):
assert self.window_size > 0
assert self.stride > 0
assert self.dilation > 0, (
"Dilation must be >0. NOTE: a dilation of 1 means no dilation."
)
@staticmethod
def pointwise_filter() -> "FilterProperties":
"""Returns filter properties for a trivial filter whose output frames
only ever depend on their respective input frame.
"""
return FilterProperties(window_size=1, stride=1)
def get_effective_size(self):
"""The number of input frames that span the window, including those
ignored by dilation.
"""
return 1 + ((self.window_size - 1) * self.dilation)
def get_convolution_padding(self):
"""The number of frames that need to be inserted on each end for a
typical convolution.
"""
if self.window_size % 2 == 0:
raise ValueError("Cannot determine padding with even window size")
if self.causal:
return self.get_effective_size() - 1
return (self.get_effective_size() - 1) // 2
def get_noncausal_equivalent(self):
"""From a causal filter definition, gets a compatible non-causal filter
definition for which each output frame depends on the same input frames,
plus some false dependencies.
"""
if not self.causal:
return self
return FilterProperties(
# NOTE: valid even on even window sizes e.g. (2-1)*2+1 == 3
window_size=(self.window_size - 1) * 2 + 1,
stride=self.stride,
dilation=self.dilation,
causal=False,
)
def with_on_top(self, other, allow_approximate=True):
"""Considering the chain of filters `other(self(x))`, returns
recalculated properties of the resulting filter.
Arguments
---------
other: FilterProperties
The filter to combine `self` with.
allow_approximate: bool, optional
If `True` (the default), the resulting properties may be
"pessimistic" and express false dependencies instead of erroring
out when exact properties cannot be determined.
This might be the case when stacking non-causal and causal filters.
Depending on the usecase, this might be fine, but functions like
`has_overlap` may erroneously start returning `True`.
Returns
-------
FilterProperties
The properties of the combined filters.
"""
self_size = self.window_size
if other.window_size % 2 == 0:
if allow_approximate:
other_size = other.window_size + 1
else:
raise ValueError(
"The filter to append cannot have an uneven window size. "
"Specify `allow_approximate=True` if you do not need to "
"analyze exact dependencies."
)
else:
other_size = other.window_size
if (self.causal or other.causal) and not (self.causal and other.causal):
if allow_approximate:
return self.get_noncausal_equivalent().with_on_top(
other.get_noncausal_equivalent()
)
else:
raise ValueError(
"Cannot express exact properties of causal and non-causal "
"filters. "
"Specify `allow_approximate=True` if you do not need to "
"analyze exact dependencies."
)
out_size = self_size + (self.stride * (other_size - 1))
stride = self.stride * other.stride
dilation = self.dilation * other.dilation
causal = self.causal
return FilterProperties(out_size, stride, dilation, causal)
def stack_filter_properties(filters, allow_approximate=True):
"""Returns the filter properties of a sequence of stacked filters.
If the sequence is empty, then a no-op filter is returned (with a size and
stride of 1).
Arguments
---------
filters: FilterProperties | any
The filters to combine, e.g. `[a, b, c]` modelling `c(b(a(x)))`.
If an item is not an instance of :class:`FilterProperties`, then this
attempts to call `.get_filter_properties()` over it.
allow_approximate: bool, optional
See `FilterProperties.with_on_top`.
Returns
-------
ret: FilterProperties
The properties of the sequence of filters
"""
ret = FilterProperties.pointwise_filter()
for prop in filters:
if not isinstance(prop, FilterProperties):
prop = prop.get_filter_properties()
ret = ret.with_on_top(prop, allow_approximate)
return ret