=======================================
Introduction to spectral graph wavelets
=======================================

This tutorial will show you how to easily construct a wavelet_ frame, a kind of
filter bank, and apply it to a signal. This tutorial will walk you into
computing the wavelet coefficients of a graph, visualizing filters in the
vertex domain, and using the wavelets to estimate the curvature of a 3D shape.

.. _wavelet: https://en.wikipedia.org/wiki/Wavelet

As usual, we first have to import some packages.

.. plot::
    :context: reset

    >>> import numpy as np
    >>> import matplotlib.pyplot as plt
    >>> from pygsp2 import graphs, filters, plotting, utils

Then we can load a graph. The graph we'll use is a nearest-neighbor graph of a
point cloud of the Stanford bunny. It will allow us to get interesting visual
results using wavelets.

.. plot::
    :context: close-figs

    >>> G = graphs.Bunny()

.. note::
    At this stage we could compute the Fourier basis using
    :meth:`pygsp2.graphs.Graph.compute_fourier_basis`, but this would take some
    time, and can be avoided with a Chebychev polynomials approximation to
    graph filtering. See the documentation of the
    :meth:`pygsp2.filters.Filter.filter` filtering function and
    :cite:`hammond2011wavelets` for details on how it is down.

Simple filtering: heat diffusion
--------------------------------

Before tackling wavelets, let's observe the effect of a single filter on a
graph signal. We first design a few heat kernel filters, each with a different
scale.

.. plot::
    :context: close-figs

    >>> taus = [10, 25, 50]
    >>> g = filters.Heat(G, taus)

Let's create a signal as a Kronecker delta located on one vertex, e.g. the
vertex 20. That signal is our heat source.

.. plot::
    :context: close-figs

    >>> s = np.zeros(G.N)
    >>> DELTA = 20
    >>> s[DELTA] = 1

We can now simulate heat diffusion by filtering our signal `s` with each of our
heat kernels.

.. plot::
    :context: close-figs

    >>> s = g.filter(s, method='chebyshev')

And finally plot the filtered signal showing heat diffusion at different
scales.

.. plot::
    :context: close-figs

    >>> fig = plt.figure(figsize=(10, 3))
    >>> for i in range(g.Nf):
    ...     ax = fig.add_subplot(1, g.Nf, i+1, projection='3d')
    ...     title = r'Heat diffusion, $\tau={}$'.format(taus[i])
    ...     _ = G.plot(s[:, i], colorbar=False, title=title, ax=ax)
    ...     ax.set_axis_off()
    >>> fig.tight_layout()

.. note::
    The :meth:`pygsp2.filters.Filter.localize` method can be used to visualize a
    filter in the vertex domain instead of doing it manually.

Visualizing wavelets atoms
--------------------------

Let's now replace the Heat filter by a filter bank of wavelets. We can create a
filter bank using one of the predefined filters, such as
:class:`pygsp2.filters.MexicanHat` to design a set of `Mexican hat wavelets`_.

.. _Mexican hat wavelets:
    https://en.wikipedia.org/wiki/Mexican_hat_wavelet

.. plot::
    :context: close-figs

    >>> g = filters.MexicanHat(G, Nf=6)  # Nf = 6 filters in the filter bank.

Then plot the frequency response of those filters.

.. plot::
    :context: close-figs

    >>> fig, ax = plt.subplots(figsize=(10, 5))
    >>> _ = g.plot(title='Filter bank of mexican hat wavelets', ax=ax)

.. note::
    We can see that the wavelet atoms are stacked on the low frequency part of
    the spectrum. A better coverage could be obtained by adapting the filter
    bank with :class:`pygsp2.filters.WarpedTranslates` or by using another
    filter bank like :class:`pygsp2.filters.Itersine`.

We can visualize the atoms as we did with the heat kernel, by filtering
a Kronecker delta placed at one specific vertex.

.. plot::
    :context: close-figs

    >>> s = g.localize(DELTA)
    >>>
    >>> fig = plt.figure(figsize=(10, 2.5))
    >>> for i in range(3):
    ...     ax = fig.add_subplot(1, 3, i+1, projection='3d')
    ...     _ = G.plot(s[:, i], title='Wavelet {}'.format(i+1), ax=ax)
    ...     ax.set_axis_off()
    >>> fig.tight_layout()

Curvature estimation
--------------------

As a last and more applied example, let us try to estimate the curvature of the
underlying 3D model by only using spectral filtering on the nearest-neighbor
graph formed by its point cloud.

A simple way to accomplish that is to use the coordinates map :math:`[x, y, z]`
and filter it using the above defined wavelets. Doing so gives us a
3-dimensional signal
:math:`[g_i(L)x, g_i(L)y, g_i(L)z], \ i \in [0, \ldots, N_f]`
which describes variation along the 3 coordinates.

.. plot::
    :context: close-figs

    >>> s = G.coords
    >>> s = g.filter(s)

The curvature is then estimated by taking the :math:`\ell_1` or :math:`\ell_2`
norm across the 3D position.

.. plot::
    :context: close-figs

    >>> s = np.linalg.norm(s, ord=2, axis=1)

Let's finally plot the result to observe that we indeed have a measure of the
curvature at different scales.

.. plot::
    :context: close-figs

    >>> fig = plt.figure(figsize=(10, 7))
    >>> for i in range(4):
    ...     ax = fig.add_subplot(2, 2, i+1, projection='3d')
    ...     title = 'Curvature estimation (scale {})'.format(i+1)
    ...     _ = G.plot(s[:, i], title=title, ax=ax)
    ...     ax.set_axis_off()
    >>> fig.tight_layout()
