Skip to content

Commit cb6cbd1

Browse files
committed
Chebyshev: coefficients computation
1 parent 6d45b02 commit cb6cbd1

File tree

3 files changed

+130
-22
lines changed

3 files changed

+130
-22
lines changed

pygsp/filters/approximations.py

Lines changed: 79 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,85 @@ class Chebyshev(Filter):
4040
4141
"""
4242

43-
def __init__(self, G, filters, order=30):
43+
def __init__(self, G, coefficients):
4444

4545
self.G = G
46-
self.order = order
4746

48-
try:
49-
self._compute_coefficients(filters)
50-
self.Nf = filters.Nf
51-
except:
52-
self._coefficients = np.asarray(filters)
53-
while self._coefficients.ndim < 3:
54-
self._coefficients = np.expand_dims(self._coefficients, -1)
55-
self.Nf = self._coefficients.shape[1]
47+
coefficients = np.asarray(coefficients)
48+
while coefficients.ndim < 3:
49+
coefficients = np.expand_dims(coefficients, -1)
50+
51+
self.Nout, self.Nin = coefficients.shape[1:]
52+
self.Nf = self.Nout * self.Nin
53+
self._coefficients = coefficients
54+
55+
# That is a factory method.
56+
@classmethod
57+
def from_filter(cls, filters, order=30, n=None):
58+
r"""Compute the Chebyshev coefficients which approximate the filters.
59+
60+
The :math:`K+1` coefficients, where :math:`K` is the polynomial order,
61+
to approximate the function :math:`f` are computed by the discrete
62+
orthogonality condition as
63+
64+
.. math:: a_k \approx \frac{2-\delta_{0k}}{N}
65+
\sum_{n=0}^{N-1} T_k(x_n) f(x_n),
66+
67+
where :math:`\delta_{ij}` is the Kronecker delta function and the
68+
:math:`x_n` are the N shifted Gauss–Chebyshev zeros of :math:`T_N(x)`,
69+
given by
70+
71+
.. math:: x_n = \frac{\lambda_\text{max}}{2}
72+
\cos\left( \frac{\pi (2k+1)}{2N} \right)
73+
+ \frac{\lambda_\text{max}}{2}.
74+
75+
For any N, these approximate coefficients provide an exact
76+
approximation to the function at :math:`x_k` with a controlled error
77+
between those points. The exact coefficients are obtained with
78+
:math:`N=\infty`, thus representing the function exactly at all points
79+
in :math:`[0, \lambda_\text{max}]`. The rate of convergence depends on
80+
the function and its smoothness.
81+
82+
Parameters
83+
----------
84+
filters : filters.Filter
85+
A filterbank (:class:`Filter`) to be approximated by a set of
86+
Chebyshev polynomials.
87+
order : int
88+
The order of the Chebyshev polynomials.
89+
n : int
90+
The number of Gauss–Chebyshev zeros used to approximate the
91+
coefficients. Defaults to the polynomial order plus one.
92+
93+
Examples
94+
--------
95+
>>> G = graphs.Ring(50)
96+
>>> G.estimate_lmax()
97+
>>> g = filters.Filter(G, lambda x: 2*x)
98+
>>> h = filters.Chebyshev.from_filter(g, order=4)
99+
>>> print(', '.join([str(int(c)) for c in h._coefficients]))
100+
4, 4, 0, 0, 0
101+
102+
"""
103+
lmax = filters.G.lmax
104+
105+
if n is None:
106+
n = order + 1
107+
108+
points = np.pi * (np.arange(n) + 0.5) / n
56109

110+
# The Gauss–Chebyshev zeros of Tk(x), scaled to [0, lmax].
111+
zeros = lmax/2 * np.cos(points) + lmax/2
112+
113+
# TODO: compute with scipy.fftpack.dct().
114+
c = np.empty((order + 1, filters.Nf))
115+
for i, kernel in enumerate(filters._kernels):
116+
for k in range(order + 1):
117+
T_k = np.cos(k * points) # Chebyshev polynomials of order k.
118+
c[k, i] = 2 / n * kernel(zeros).dot(T_k)
119+
c[0, :] /= 2
120+
121+
return cls(filters.G, c)
57122

58123
@staticmethod
59124
def scale_data(x, lmax):
@@ -106,13 +171,6 @@ def _filter(self, s, method, _):
106171
except AttributeError:
107172
raise ValueError('Unknown method {}.'.format(method))
108173

109-
def _compute_coefficients(self, filters):
110-
r"""Compute the coefficients of the Chebyshev series approximating the filters.
111-
112-
Some implementations define c_0 / 2.
113-
"""
114-
pass
115-
116174
def _evaluate_direct(self, c, x):
117175
r"""Evaluate Fout*Fin polynomials at each value in x."""
118176
K, F = c.shape[:2]
@@ -125,13 +183,13 @@ def _evaluate_direct(self, c, x):
125183
def _evaluate_recursive(self, c, x):
126184
"""Evaluate a Chebyshev series for y. Optionally, times s.
127185
128-
.. math: p(y) = \sum_{k=0}^{K} a_k * T_k(y) * s
186+
.. math:: p(x) = \sum_{k=0}^{K} a_k * T_k(x) * s
129187
130188
Parameters
131189
----------
132190
c: array-like
133191
set of Chebyshev coefficients. (size K x F where K is the polynomial order, F is the number of filters)
134-
y: array-like
192+
x: array-like
135193
vector to be evaluated. (size N x 1)
136194
vector or matrix
137195
signal: array-like
@@ -171,7 +229,7 @@ def mult(c, x):
171229
def dot(L, x):
172230
"""One diffusion step by multiplication with the Laplacian."""
173231
x.shape = (M * Fin, N)
174-
return L.__rmul__(x) # x @ L
232+
return L.__rmatmul__(x) # x @ L
175233

176234
x0 = s.view()
177235
result = mult(c[0], x0)
@@ -195,7 +253,7 @@ def mult(c, s):
195253
def dot(L, x):
196254
"""One diffusion step by multiplication with the Laplacian."""
197255
x.shape = (M * Fout, N)
198-
y = L.__rmul__(x) # x @ L
256+
y = L.__rmatmul__(x) # x @ L
199257
x.shape = (M, Fout, N)
200258
y.shape = (M, Fout, N)
201259
return y

pygsp/filters/filter.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,27 @@ def _evaluate(self, x, _):
9999
y[i] = kernel(x)
100100
return y
101101

102+
def approximate(self, method, **kwargs):
103+
r"""Returns a filter which approximates this filter.
104+
105+
While approximations might loose accuracy, they allow for much faster
106+
computations.
107+
108+
Parameters
109+
----------
110+
method : str
111+
Approximation method. Only 'Chebyshev' is supported for now.
112+
kwargs : dict
113+
Parameters for the approximation method.
114+
115+
Examples
116+
--------
117+
TODO: approx plot from notebook (needs new plotting)
118+
119+
"""
120+
from . import approximations
121+
return getattr(approximations, method).from_filter(self, **kwargs)
122+
102123
def filter(self, s, method=None, order=30):
103124
r"""Filter signals (analysis or synthesis).
104125

pygsp/tests/test_filters.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,35 @@ def test_filter_methods(self, K=30, Fin=5, Fout=6, M=100):
299299
y1 = f.filter(x, method='recursive')
300300
y2 = f.filter(x, method='clenshaw')
301301
self.assertTupleEqual(y1.shape, (M, Fout, self._G.N))
302-
np.testing.assert_allclose(y1, y2)
302+
np.testing.assert_allclose(y1, y2, rtol=1e-5)
303303
# Unknown method.
304304
self.assertRaises(ValueError, f.filter, x, method='unk')
305+
306+
def test_coefficients(self, K=10, slope=3.14):
307+
r"""Test that the computed Chebyshev coefficients are correct."""
308+
# Identity filter.
309+
f = filters.Heat(self._G, tau=0)
310+
f = filters.Chebyshev.from_filter(f, order=K)
311+
c = f._coefficients.squeeze()
312+
np.testing.assert_allclose(c, [1] + K*[0], atol=1e-12)
313+
# Linear filter.
314+
f = filters.Filter(self._G, lambda x: slope*x)
315+
f = filters.Chebyshev.from_filter(f, order=K)
316+
c1 = f._coefficients.squeeze()
317+
c2 = slope / 2 * self._G.lmax
318+
np.testing.assert_allclose(c1, 2*[c2] + (K-1)*[0], atol=1e-12)
319+
320+
def test_approximations(self, N=100, K=20):
321+
r"""Test that the approximations are not far from the exact filters."""
322+
# Evaluation.
323+
x = self._rs.uniform(0, self._G.lmax, N)
324+
f1 = filters.Heat(self._G)
325+
y1 = f1.evaluate(x)
326+
f2 = f1.approximate('Chebyshev', order=K)
327+
y2 = f2.evaluate(x)
328+
np.testing.assert_allclose(y2, y1.squeeze())
329+
# Filtering.
330+
x = self._rs.uniform(size=(1, 1, self._G.N))
331+
y1 = f1.filter(x.T).T
332+
y2 = f2.filter(x)
333+
np.testing.assert_allclose(y2.squeeze(), y1)

0 commit comments

Comments
 (0)