---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.4
kernelspec:
  display_name: Python 3 (ipykernel)
  language: python
  name: python3
---

```{code-cell} ipython3
:tags: [hide-input]

%matplotlib inline
import warnings

# Ignore the first specific warning
warnings.filterwarnings(
    "ignore",
    message="plotting functions contained within `_documentation_utils` are intended for nemos's documentation.",
    category=UserWarning,
)

# Ignore the second specific warning
warnings.filterwarnings(
    "ignore",
    message="Ignoring cached namespace 'core'",
    category=UserWarning,
)

warnings.filterwarnings(
    "ignore",
    message=(
        "invalid value encountered in div "
    ),
    category=RuntimeWarning,
)
```

(convolution_background)=
# Convolution

## Generate synthetic data
Generate some simulated spike counts.

```{code-cell} ipython3
import matplotlib.patches as patches
import matplotlib.pylab as plt
import numpy as np

import nemos as nmo

np.random.seed(10)
ws = 10
# samples
n_samples = 100

spk = np.random.poisson(lam=0.1, size=(n_samples, ))

# add borders (extreme case, general border effect are represented)
spk[0] = 1
spk[3] = 1
spk[-1] = 1
spk[-4] = 1
```

## Causal, Anti-Causal, and Acausal Predictors

To construct a temporal predictor with a specified causality, we convolve a time series with one or more filters using `"valid"` mode, then pad the result according to the desired direction of causality—causal, anti-causal, or acausal.

:::{note}
The `"valid"` mode of convolution only calculates the product when the two input vectors overlap completely,
avoiding border artifacts. The outcome of such a convolution will
be an array of `max(M,N) - min(M,N) + 1` elements in length, where `M` and `N` represent the number
of elements in the arrays being convolved. For more detailed information on this,
see [jax.numpy.convolve](https://jax.readthedocs.io/en/latest/_autosummary/jax.numpy.convolve.html).
:::

A causal filter captures how an event or task variable influences the future firing-rate.
An example usage case would be that of characterizing the refractory period of a neuron
(i.e. the drop in firing rate  immediately after a spike event). Another example could be characterizing how
the current position of an animal in a maze would affect its future spiking activity.

On the other hand, if we are interested in capturing the firing rate modulation before an event occurs we may want
to use an anti-causal filter. An example of that may be the preparatory activity of pre-motor cortex that happens
before a movement is initiated (here the event is. "movement onset").

Finally, if one wants to capture both causal
and anti-causal effects one should use the acausal filters.
Below we provide the function [`create_convolutional_predictor`](nemos.convolve.create_convolutional_predictor) that runs the convolution in "valid" mode and pads the convolution output
for the different filter types.

```{code-cell} ipython3
# create three filters
basis_obj = nmo.basis.RaisedCosineLinearEval(n_basis_funcs=3)
_, w = basis_obj.evaluate_on_grid(ws)

# pad according to the causal direction of the filter, after squeeze,
# the dimension is (n_filters, n_samples)
spk_causal_conv = nmo.convolve.create_convolutional_predictor(
        w, spk, predictor_causality="causal"
)
spk_anticausal_conv = nmo.convolve.create_convolutional_predictor(
        w, spk, predictor_causality="anti-causal"
)
spk_acausal_conv = nmo.convolve.create_convolutional_predictor(
        w, spk, predictor_causality="acausal"
)
```

Plot the results

```{code-cell} ipython3
# NaN padded area
rect_causal = patches.Rectangle((0, -2.5), ws, 5, alpha=0.3, color='grey')
rect_anticausal = patches.Rectangle((len(spk)-ws, -2.5), ws, 5, alpha=0.3, color='grey')
rect_acausal_left = patches.Rectangle((0, -2.5), (ws-1)//2, 5, alpha=0.3, color='grey')
rect_acausal_right = patches.Rectangle((len(spk) - (ws-1)//2, -2.5), (ws-1)//2, 5, alpha=0.3, color='grey')

# Set this figure as the thumbnail
# mkdocs_gallery_thumbnail_number = 2

fig = plt.figure(figsize=(6, 4))

shift_spk = - spk - 0.1
ax = plt.subplot(311)

plt.title('valid + nan-pad')
ax.add_patch(rect_causal)
plt.vlines(np.arange(spk.shape[0]), 0, shift_spk, color='k')
plt.plot(np.arange(spk.shape[0]), spk_causal_conv)
plt.ylabel('causal')

ax = plt.subplot(312)
ax.add_patch(rect_anticausal)
plt.vlines(np.arange(spk.shape[0]), 0, shift_spk, color='k')
plt.plot(np.arange(spk.shape[0]), spk_anticausal_conv)
plt.ylabel('anti-causal')

ax = plt.subplot(313)
ax.add_patch(rect_acausal_left)
ax.add_patch(rect_acausal_right)
plt.vlines(np.arange(spk.shape[0]), 0, shift_spk, color='k')
plt.plot(np.arange(spk.shape[0]), spk_acausal_conv)
plt.ylabel('acausal')
plt.tight_layout()
```

```{code-cell} ipython3
:tags: [hide-input]

# save image for thumbnail
from pathlib import Path
import os

root = os.environ.get("READTHEDOCS_OUTPUT")
if root:
   path = Path(root) / "html/_static/thumbnails/background"
# if local store in ../_build/html/...
else:
   path = Path("../_build/html/_static/thumbnails/background")

# make sure the folder exists if run from build
if root or Path("../assets/stylesheets").exists():
   path.mkdir(parents=True, exist_ok=True)

if path.exists():
  fig.savefig(path / "plot_03_1D_convolution.svg")
```

## Convolve using [`Basis.compute_features`](nemos.basis._basis.Basis.compute_features)

Every basis in the `nemos.basis` module whose class name ends with "Conv" will perform a 1D convolution over the
provided input when the `compute_features` method is called. The basis elements will be used as filters for the
convolution.

All the parameters of [`create_convolutional_predictor`](nemos.convolve.create_convolutional_predictor) can be passed to the object directly at initialization.
Let's see how we can get the same results through [`Basis`](nemos.basis._basis.Basis).

```{code-cell} ipython3
# define basis with different predictor causality
causal_basis = nmo.basis.RaisedCosineLinearConv(
        n_basis_funcs=3, window_size=ws,
        conv_kwargs=dict(predictor_causality="causal")

)

acausal_basis = nmo.basis.RaisedCosineLinearConv(
        n_basis_funcs=3, window_size=ws,
        conv_kwargs=dict(predictor_causality="acausal")
)

anticausal_basis = nmo.basis.RaisedCosineLinearConv(
        n_basis_funcs=3, window_size=ws,
        conv_kwargs=dict(predictor_causality="anti-causal")
)

# compute convolutions
basis_causal_conv = causal_basis.compute_features(spk)
basis_acausal_conv = acausal_basis.compute_features(spk)
basis_anticausal_conv = anticausal_basis.compute_features(spk)
```
