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

# Introduction to Deconvolution using MCMC Methods


This notebooks is a short introduction to the basics of the LIRA algorithm and the Bayesian / MCMC approach to the deconvolution problem. It uses a simple, pure Numpy implementation of the classical [Metropolis- Hastings](https://en.wikipedia.org/wiki/Metropolis–Hastings_algorithm) algorithm to sample from the posteriror distribution.

We start with defining the required imports:

```{code-cell} ipython3
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from pylira.utils.convolution import fftconvolve
from pylira.data import gauss_and_point_sources_gauss_psf
from pylira.utils.plot import plot_example_dataset
```

To get reproducible results we seed the Numpy random number generation first:

```{code-cell} ipython3
rng = np.random.default_rng(765)
```

For this example we use one of the builtin datasets consisting of a central extend Gaussian source and near-by point sources with varying itensity:

```{code-cell} ipython3
data = gauss_and_point_sources_gauss_psf()
plot_example_dataset(data=data)
```

We start by defining the model function to compute the predicted number of counts from the given flux image, backgroun, point spread function and exposure:

```{code-cell} ipython3
def compute_npred(flux, psf, background, exposure):
    """Compute predicted number of counts"""
    npred = fftconvolve((flux + background) * exposure, psf)
    # The FFT can produce noise, which could cause values to be negative
    npred = np.clip(npred, 0, np.inf)
    return npred
```

Now we continue by defining the likelihood, prior and posterior functions.

The data is Poissonion in nature, so the likelihood is computed using the so called "Cash" statistics, see e.g. https://cxc.cfa.harvard.edu/sherpa/statistics/#cash for details.

```{code-cell} ipython3
def cash(counts, npred):
    """Cash statistics"""
    npred = np.clip(npred, 1e-25, np.inf)
    stat = 2 * (npred - counts * np.log(npred))
    return stat
```

The log prior is assumed to be constant for now:

```{code-cell} ipython3
def log_prior(flux):
    """Log prior"""
    # currently a flat prior
    return np.log(1)


def log_likelihood(flux, psf, background, counts, exposure):
    """Log likelihood"""
    npred = compute_npred(flux, psf, background, exposure)
    return cash(counts, npred).sum()


def log_posterior(flux, psf, background, counts, exposure):
    """Log posterior"""
    return log_prior(flux) + log_likelihood(
        flux=flux, psf=psf, background=background, counts=counts, exposure=exposure
    )
```

To solve the problem each pixel in the flux image is treated as and independent parameter, from which we can sample. For this we use a simple pure Numpy based implementation of the [Metropolis- Hastings](https://en.wikipedia.org/wiki/Metropolis–Hastings_algorithm) algorithm. For fast convergence we choose to sample the pixel wise flux valeus from a Gamma distribution. To avoid negative flux values and the alogithm getting stuck on a zero flux, we clip the current flux values at `flux_min`:

```{code-cell} ipython3
def random_coin(p):
    unif = np.random.uniform(0, 1)
    return unif <= p


def deconvolve_mcmc(log_posterior, flux_init, data, n_samples, flux_min=1e-2):
    """Deconvolve using Baysian MCMC"""
    image_trace, log_post_trace = [], []

    while len(image_trace) < n_samples:
        flux = rng.gamma(np.clip(flux_init, flux_min, np.inf))

        prob = log_posterior(flux=flux, **data)
        prob_init = log_posterior(flux=flux_init, **data)

        acceptance = min(np.exp(prob_init - prob), 1)

        if random_coin(acceptance) and np.isfinite(prob):
            flux_init = flux

        image_trace.append(flux)
        log_post_trace.append(prob)

    return {
        "image-trace": np.array(image_trace),
        "log-posterior-trace": np.array(log_post_trace),
    }
```

As starting values for the flux we use the scaled data themselves and then we sample 50.000 ierations to get sufficient statistics:

```{code-cell} ipython3
%%time

flux_init = (data["counts"] - data["background"]) / data["exposure"]

data.pop("flux", None)

result = deconvolve_mcmc(
    log_posterior=log_posterior,
    flux_init=flux_init,
    data=data,
    n_samples=50_000
)
```

To check the global convergence we can plot the trace of the log posterior:

```{code-cell} ipython3
plt.plot(result["log-posterior-trace"])
```

And finally plot the mean posterior as and estimate for the deconvolved flux:

```{code-cell} ipython3
---
nbsphinx-thumbnail:
  tooltip: Learn the basics of the MCMC deconvolution.
---
n_burn_in = 5000
posterior_mean = np.mean(result["image-trace"][n_burn_in:], axis=0)
plt.imshow(posterior_mean, origin="lower")
plt.colorbar()
```
