diff --git a/data/poisson.npz b/data/poisson.npz
new file mode 100644
index 0000000000000000000000000000000000000000..456134a992071c2a6bcb91a8377cc9dae90a5a7f
Binary files /dev/null and b/data/poisson.npz differ
diff --git a/demo_poisson.ipynb b/demo_poisson.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..de9b7f8d22698c07e3efa15e5d4ab0146063664f
--- /dev/null
+++ b/demo_poisson.ipynb
@@ -0,0 +1,208 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Nifty tutorial for Poisson count data\n",
+    "====================================="
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Setup\n",
+    "-----"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy as np\n",
+    "import matplotlib.pyplot as plt\n",
+    "import nifty8 as ift\n",
+    "from utils import plot_2D, load_psf, geovi_sampling, plot_posterior\n",
+    "\n",
+    "ift.random.push_sseq_from_seed(42)\n",
+    "evidences = []"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Load data and visualize\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "position_space = ift.RGSpace([128, 128])\n",
+    "\n",
+    "# Homogeneous poisson process\n",
+    "\n",
+    "\n",
+    "print(model1)\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Set up likelihood & PSF\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Inference model 1\n",
+    "\n",
+    "\n",
+    "print(evidence)\n",
+    "evidences += [evidence, ]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Independent poisson process\n",
+    "\n",
+    "# Inference model 2\n",
+    "\n",
+    "\n",
+    "evidences += [evidence, ]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Compare evidence\n",
+    "print(evidences)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Diffuse poisson process\n",
+    "args = {\n",
+    "    'offset_mean': .5,\n",
+    "    'offset_std': (1., 1E-5),\n",
+    "\n",
+    "    # Amplitude of field fluctuations\n",
+    "    'fluctuations': (1.5, 0.5),  # 1.0, 1e-2\n",
+    "\n",
+    "    # Exponent of power law power spectrum component\n",
+    "    'loglogavgslope': (-4., 1),  # -6.0, 1\n",
+    "\n",
+    "    # Amplitude of integrated Wiener process power spectrum component\n",
+    "    'flexibility': (1., 0.2),  # 2.0, 1.0\n",
+    "\n",
+    "    # How ragged the integrated Wiener process component is\n",
+    "    'asperity': (0.1, 0.01),  # 0.1, 0.5\n",
+    "\n",
+    "    # Name of the input keys\n",
+    "    'prefix' : 'diffuse'\n",
+    "}\n",
+    "correlated_field = ift.SimpleCorrelatedField(position_space, **args)\n",
+    "pspec = correlated_field.power_spectrum\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Prior samples\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Inference model 3\n",
+    "\n",
+    "evidences += [evidence, ]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Compare evidence\n",
+    "print(evidences)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Posterior visualization\n",
+    "-----------------------"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "plot_posterior(samples, data, model3, diffuse, model2, pspec)"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.10.6"
+  },
+  "vscode": {
+   "interpreter": {
+    "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6"
+   }
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/demo_poisson_solution.ipynb b/demo_poisson_solution.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..98a883a8f07d02cf17c71d4d885841ff1ac136be
--- /dev/null
+++ b/demo_poisson_solution.ipynb
@@ -0,0 +1,229 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Nifty tutorial for Poisson count data\n",
+    "====================================="
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Setup\n",
+    "-----"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy as np\n",
+    "import matplotlib.pyplot as plt\n",
+    "import nifty8 as ift\n",
+    "from utils import plot_2D, load_psf, geovi_sampling, plot_posterior\n",
+    "\n",
+    "ift.random.push_sseq_from_seed(42)\n",
+    "evidences = []"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Load data and visualize\n",
+    "data = np.load('data/poisson.npz')\n",
+    "print(data['data'].shape)\n",
+    "plot_2D(data['data'], 'Data')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "position_space = ift.RGSpace([128, 128])\n",
+    "\n",
+    "# Homogeneous poisson process\n",
+    "projection = ift.VdotOperator(ift.full(position_space, 1.)).adjoint\n",
+    "model1 = ift.FieldAdapter(projection.domain, 'hom')\n",
+    "model1 = ift.exp(5. * model1)\n",
+    "model1 = projection @ model1\n",
+    "\n",
+    "print(model1)\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Set up likelihood & PSF\n",
+    "d = ift.makeField(position_space, data['data'])\n",
+    "likelihood = ift.PoissonianEnergy(d)\n",
+    "PSF_op, psf = load_psf(position_space)\n",
+    "plot_2D(psf, 'PSF')\n",
+    "likelihood = likelihood @ PSF_op"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Inference model 1\n",
+    "samples, evidence = geovi_sampling(likelihood @ model1)\n",
+    "plot_2D(samples.average(model1).val, 'model1 posterior mean')\n",
+    "print(evidence)\n",
+    "evidences += [evidence, ]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Independent poisson process\n",
+    "model2 = ift.InverseGammaOperator(position_space, 2., 3.).ducktape('independent')\n",
+    "\n",
+    "# Inference model 2\n",
+    "samples, evidence = geovi_sampling(likelihood @ model2)\n",
+    "plot_2D(samples.average(model2).val, 'model2 posterior mean')\n",
+    "print(evidence)\n",
+    "evidences += [evidence, ]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Compare evidence\n",
+    "print(evidences)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Diffuse poisson process\n",
+    "args = {\n",
+    "    'offset_mean': .5,\n",
+    "    'offset_std': (1., 1E-5),\n",
+    "\n",
+    "    # Amplitude of field fluctuations\n",
+    "    'fluctuations': (1.5, 0.5),  # 1.0, 1e-2\n",
+    "\n",
+    "    # Exponent of power law power spectrum component\n",
+    "    'loglogavgslope': (-4., 1),  # -6.0, 1\n",
+    "\n",
+    "    # Amplitude of integrated Wiener process power spectrum component\n",
+    "    'flexibility': (1., 0.2),  # 2.0, 1.0\n",
+    "\n",
+    "    # How ragged the integrated Wiener process component is\n",
+    "    'asperity': (0.1, 0.01),  # 0.1, 0.5\n",
+    "\n",
+    "    # Name of the input keys\n",
+    "    'prefix' : 'diffuse'\n",
+    "}\n",
+    "correlated_field = ift.SimpleCorrelatedField(position_space, **args)\n",
+    "pspec = correlated_field.power_spectrum\n",
+    "diffuse = correlated_field.exp()\n",
+    "\n",
+    "model3 = diffuse + model2"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Prior samples\n",
+    "pl = ift.Plot()\n",
+    "for _ in range(9):\n",
+    "    pl.add(model3(ift.from_random(model3.domain)))\n",
+    "pl.output()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Inference model 3\n",
+    "samples, evidence = geovi_sampling(likelihood @ model3)\n",
+    "plot_2D(samples.average(model3).val, 'model3 posterior mean')\n",
+    "print(evidence)\n",
+    "evidences += [evidence, ]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Compare evidence\n",
+    "print(evidences)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Posterior visualization\n",
+    "-----------------------"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "plot_posterior(samples, data, model3, diffuse, model2, pspec)"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.10.6"
+  },
+  "vscode": {
+   "interpreter": {
+    "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6"
+   }
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..e84fbc182f26809450d53ec8cb535ccd9a2dff6e
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,120 @@
+import nifty8 as ift
+import numpy as np
+import matplotlib.pyplot as plt
+
+def load_psf(space):
+    hsp = space.get_default_codomain()
+    psf = lambda k: 1./(1.+(k/20.)**2)/space.size
+    PD = ift.PowerDistributor(hsp)
+    psf = ift.PS_field(PD.domain[0], psf)
+    psf = PD(psf)
+    ht = ift.HartleyOperator(hsp, space)
+    pos_psf = ht(psf).val
+    pos_psf = np.roll(pos_psf, pos_psf.shape[0]//2, axis = 0)
+    pos_psf = np.roll(pos_psf, pos_psf.shape[1]//2, axis = 1)
+    pos_psf = ift.makeField(space, pos_psf)
+    R = ht @ ift.DiagonalOperator(psf) @ ht.adjoint
+    return R, pos_psf.val
+
+def plot_2D(inp, label):
+    fig, ax = plt.subplots(nrows=1, ncols=1, figsize = (10, 8))
+    im = ax.imshow(inp.T, origin='lower', extent=[0,1,0,1])
+    ax.set_title(label)
+    fig.colorbar(im, ax=ax)
+    plt.show()
+
+def geovi_sampling(likelihood):
+    ic_samp = ift.AbsDeltaEnergyController(1E-3, iteration_limit = 30)
+    ic_sampnl = ift.AbsDeltaEnergyController(0.1, iteration_limit = 20,
+                                            convergence_level=2)
+    mini_samp = ift.NewtonCG(ic_sampnl)
+    ic_mini = ift.AbsDeltaEnergyController(0.1, iteration_limit=15,
+                                            name = 'Minimizer')
+    minimizer = ift.NewtonCG(ic_mini)
+
+    N_samples = 4
+    iteration_limit = 5
+    initial_mean = 0.1 * ift.from_random(likelihood.domain)
+    samples = ift.optimize_kl(likelihood, iteration_limit, N_samples,
+                            minimizer, ic_samp, mini_samp,
+                            initial_position=initial_mean)
+    ev, _ = ift.estimate_evidence_lower_bound(
+    ift.StandardHamiltonian(likelihood), samples, 
+    min(100, likelihood.domain.size), verbose = False)
+    return samples, ev.average().val[()]
+
+def plot_posterior(samples, data, model3, diffuse, model2, pspec):
+    fig, ax = plt.subplots(nrows=5, ncols=3, figsize = (20, 28))
+
+    ax[0,0].set_visible(False)
+    ax[0,2].set_visible(False)
+    ax[4,0].set_visible(False)
+    ax[4,2].set_visible(False)
+
+    im = ax[0,1].imshow(data['data'].T, origin='lower', extent=[0,1,0,1])
+    ax[0,1].set_title('Data')
+    fig.colorbar(im, ax=ax[0,1])
+
+    im = ax[1,0].imshow(data['sky'].T, origin='lower', extent=[0,1,0,1])
+    ax[1,0].set_title('Ground truth')
+    fig.colorbar(im, ax=ax[1,0])
+
+    im = ax[1,1].imshow(samples.average(model3.force).val.T, origin='lower', extent=[0,1,0,1])
+    ax[1,1].set_title('Posterior mean')
+    fig.colorbar(im, ax=ax[1,1])
+    sm = tuple(s for s in samples.iterator(model3.force))
+    im = ax[1,2].imshow(sm[0].val.T, origin='lower', extent=[0,1,0,1])
+    ax[1,2].set_title('Posterior sample')
+    fig.colorbar(im, ax=ax[1,2])
+
+
+    im = ax[2,0].imshow(data['points'].T, origin='lower', extent=[0,1,0,1])
+    ax[2,0].set_title('Ground truth (sources)')
+    fig.colorbar(im, ax=ax[2,0])
+
+    im = ax[2,1].imshow(samples.average(model2.force).val.T, origin='lower', extent=[0,1,0,1])
+    ax[2,1].set_title('Posterior mean (sources)')
+    fig.colorbar(im, ax=ax[2,1])
+
+    sm = tuple(s for s in samples.iterator(model2.force))
+    im = ax[2,2].imshow(sm[0].val.T, origin='lower', extent=[0,1,0,1])
+    ax[2,2].set_title('Posterior sample (sources)')
+    fig.colorbar(im, ax=ax[2,2])
+
+
+    im = ax[3,0].imshow(data['diffuse'].T, origin='lower', extent=[0,1,0,1])
+    ax[3,0].set_title('Ground truth (diffuse)')
+    fig.colorbar(im, ax=ax[3,0])
+
+    im = ax[3,1].imshow(samples.average(diffuse.force).val.T, origin='lower', extent=[0,1,0,1])
+    ax[3,1].set_title('Posterior mean (diffuse)')
+    fig.colorbar(im, ax=ax[3,1])
+
+    sm = tuple(s for s in samples.iterator(diffuse.force))
+    im = ax[3,2].imshow(sm[0].val.T, origin='lower', extent=[0,1,0,1])
+    ax[3,2].set_title('Posterior sample (diffuse)')
+    fig.colorbar(im, ax=ax[3,2])
+
+    ax = ax[4,1]
+    ks = pspec.target[0].k_lengths
+    lbl = 'posterior samples'
+    for i,s in enumerate(samples.iterator()):
+        ss = pspec.force(s)
+        ax.plot(ks[1:], ss.val[1:], color='k', alpha = 0.4,
+                label=lbl)
+        lbl=None
+
+    ax.plot(ks[1:],data['pspec'][1:], color = 'g',label='ground truth')
+    mm = samples.average((pspec.log()).force)
+    ax.plot(ks[1:], mm.exp().val[1:], color='r',label='posterior mean')
+    #ax.set_ylim([1E-2*np.min(pp.val[1:]), 10*np.max(pp.val[1:])])
+    ax.set_xscale('log')
+    ax.set_yscale('log')
+    ax.set_xlabel(r'$|k|$')
+    ax.set_ylabel(r'$P_s\left(|k|\right)$')
+    ax.set_title('Power spectrum')
+    leg = ax.legend()
+    #for lh in leg.legendHandles: 
+    #    lh.set_alpha(1)
+    fig.tight_layout()
+    plt.show()