In this notebook, we'll take a closer look at the X-ray image data products, and build a simple, generative, forward model for the observed data.
In [1]:
import astropy.io.fits as pyfits
import astropy.visualization as viz
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
plt.rcParams['figure.figsize'] = (10.0, 10.0)
In [2]:
imfits = pyfits.open('a1835_xmm/P0098010101M2U009IMAGE_3000.FTZ')
im = imfits[0].data
im
is the image, our observed data, presented after some "standard processing." The numbers in the pixels are counts (i.e. numbers of photoelectrons recorded by the CCD during the exposure).
In [3]:
plt.imshow(viz.scale_image(im, scale='log', max_cut=40), cmap='gray', origin='lower');
$S(r) = S_0 \left[1.0 + \left(\frac{r}{r_c}\right)^2\right]^{-3\beta + 1/2}$,
where $r$ is projected distance from the cluster center.
The parameters of this model are:
ex
. Counts can be produced by:
CL*ex
, where CL
is an image with pixel values computed from $S(r)$.b*ex
, where b
is a single number with units of counts/s/pixel.pb
.(CL+b)*ex + pb
predicts an expected number of counts/pixel across the field. What does this mean? Blah blah Poisson distribution, etc.
In [4]:
pbfits = pyfits.open('a1835_xmm/P0098010101M2X000BKGMAP3000.FTZ')
pb = pbfits[0].data
exfits = pyfits.open('a1835_xmm/P0098010101M2U009EXPMAP3000.FTZ')
ex = exfits[0].data
ex
image is in units of seconds, and represents the effective exposure time at each pixel position.
In [5]:
plt.imshow(ex, cmap='gray', origin='lower');
pb
is not data at all, but rather a model for the expected counts/pixel in this specific observation due to the "quiescent particle background."pb
.
In [6]:
plt.imshow(pb, cmap='gray', origin='lower');
In [7]:
mask = np.loadtxt('a1835_xmm/M2ptsrc.txt')
for reg in mask:
# this is inefficient but effective
for i in np.round(reg[1]+np.arange(-np.ceil(reg[2]),np.ceil(reg[2]))):
for j in np.round(reg[0]+np.arange(-np.ceil(reg[2]),np.ceil(reg[2]))):
if (i-reg[1])**2 + (j-reg[0])**2 <= reg[2]**2:
ex[np.int(i-1), np.int(j-1)] = 0.0
In [8]:
plt.imshow(ex, cmap='gray', origin='lower');
${\rm Pr}(N_k|\mu_k) = \frac{{\rm e}^{-\mu} \mu^{N_k}}{N_k !}$
$\mu_k(\theta) = \left( S(r_k;\theta) + b \right) \cdot$ ex + pb
In [9]:
# import cluster_pgm
# cluster_pgm.forward()
from IPython.display import Image
Image(filename="cluster_pgm_forward.png")
Out[9]:
In [10]:
def beta_model_profile(r, S0, rc, beta):
'''
The fabled beta model, radial profile S(r)
'''
return S0 * (1.0 + (r/rc)**2)**(-3.0*beta + 0.5)
def beta_model_image(x, y, x0, y0, S0, rc, beta):
'''
Here, x and y are arrays ("meshgrids" or "ramps") containing x and y pixel numbers,
and the other arguments are galaxy cluster beta model parameters.
Returns a surface brightness image of the same shape as x and y.
'''
return beta_model_profile(np.sqrt((x-x0)**2 + (y-y0)**2), S0, rc, beta)
def model_image(x, y, ex, pb, x0, y0, S0, rc, beta, b):
'''
Here, x, y, ex and pb are images, all of the same shape, and the other args are
cluster model and X-ray background parameters. ex is the (constant) exposure map
and pb is the (constant) particle background map.
'''
return (beta_model_image(x, y, x0, y0, S0, rc, beta) + b) * ex + pb
In [11]:
# Set up the ramp images, to enable fast array calculations:
nx,ny = ex.shape
x = np.outer(np.ones(ny),np.arange(nx))
y = np.outer(np.arange(ny),np.ones(nx))
fig,ax = plt.subplots(nrows=1, ncols=2)
fig.set_size_inches(15, 6)
plt.subplots_adjust(wspace=0.2)
left = ax[0].imshow(x, cmap='gray', origin='lower')
ax[0].set_title('x')
fig.colorbar(left,ax=ax[0],shrink=0.9)
right = ax[1].imshow(y, cmap='gray', origin='lower')
ax[1].set_title('y')
fig.colorbar(right,ax=ax[1],shrink=0.9)
Out[11]:
In [20]:
# Now choose parameters, compute model and plot, compared to data!
x0,y0 = 328,328 # The center of the image is 328,328
S0,b = 0.001,1e-2 # Cluster and background surface brightness, arbitrary units
beta = 2.0/3.0 # Canonical value is beta = 2/3
rc = 12 # Core radius, in pixels
# Realize the expected counts map for the model:
mu = model_image(x,y,ex,pb,x0,y0,S0,rc,beta,b)
In [21]:
# Draw a *sample image* from the Poisson sampling distribution:
mock = np.random.poisson(mu,mu.shape)
# The difference between the mock and the real data should be symmetrical noise if the model
# is a good match...
diff = im - mock
# Plot three panels:
fig,ax = plt.subplots(nrows=1, ncols=3)
fig.set_size_inches(15, 6)
plt.subplots_adjust(wspace=0.2)
left = ax[0].imshow(viz.scale_image(mock, scale='log', max_cut=40), cmap='gray', origin='lower')
ax[0].set_title('Mock (log, rescaled)')
fig.colorbar(left,ax=ax[0],shrink=0.6)
center = ax[1].imshow(viz.scale_image(im, scale='log', max_cut=40), cmap='gray', origin='lower')
ax[1].set_title('Data (log, rescaled)')
fig.colorbar(center,ax=ax[1],shrink=0.6)
right = ax[2].imshow(diff, vmin=-40, vmax=40, cmap='gray', origin='lower')
ax[2].set_title('Difference (linear)')
fig.colorbar(right,ax=ax[2],shrink=0.6)
Out[21]: