Spatial Priors#

In this tutorial, we’ll give an overview of how to perform spatial prior analysis.

[14]:
import numpy as np
import matplotlib.pyplot as plt
import healpy as hp

import histlite as hl
import csky as cy

%matplotlib inline
[15]:
cy.plotting.mrichman_mpl()
[16]:
ana_dir = '/data/user/mrichman/csky_cache/ana'

We’ll test with one year of data, PS IC86-2011.

[17]:
ana = cy.get_analysis(cy.selections.repo, 'version-003-p02', cy.selections.PSDataSpecs.ps_2011, dir=ana_dir)
Setting up Analysis for:
IC86_2011
Setting up IC86_2011...
<- /data/user/mrichman/csky_cache/ana/IC86_2011.subanalysis.version-003-p02.npy
Done.

Everything that follows will use this same Analysis, so we put it in cy.CONF.

[18]:
cy.CONF['ana'] = ana

Arbitrary prior#

We’ll use histlite to create an arbitrary prior:

[19]:
m = hl.heal.hist(128, [0], [np.pi]).smoothing(np.radians(10)).normalize()
m.map = np.maximum(1e-4, m.map)
[20]:
fig, ax = plt.subplots (subplot_kw=dict (projection='aitoff'))
sp = cy.plotting.SkyPlotter(pc_kw=dict())
mesh, cb = sp.plot_map(ax, np.log10(m.map), n_ticks=2)
kw = dict(color='.5', alpha=.5)
sp.plot_gp(ax, lw=.5, **kw)
sp.plot_gc(ax, **kw)
ax.grid(**kw)
cb.set_label(r'spatial prior (log scale, a.u.)')
plt.tight_layout()
_images/08_spatial_priors_11_0.png

SpatialPriorTrialRunner#

We’ll test using an nside=64 version of the prior map, just to keep things snappy.

We also need to give it a value for src_tr, to keep the spatial prior trial runner happy. Here, we’ll pass it ra=pi, dec=0.0, which is the best-fit of the map we made. We also need to keep the energy in the conf, for running trials.

[21]:
prior = np.where(m.map >= m.map.min(), m.map, 0)
dec85 = np.radians(85)
sptr = cy.get_spatial_prior_trial_runner(llh_priors=hp.ud_grade(m.map, 64), min_dec=-dec85, max_dec=dec85,
                                         src_tr=cy.sources(np.pi,0.), conf={"extra_keep":["energy"]})

Now we can run a trial straight away:

[22]:
%time result = sptr.get_one_fit(20, seed=1, mp_cpus=15, logging=True)
result
Scanning 48984 locations using 15 cores:
      48984/48984 coordinates complete.
CPU times: user 598 ms, sys: 376 ms, total: 974 ms
Wall time: 1min 1s
[22]:
array([4.70029474e+01, 4.70029474e+01, 2.30528063e+01, 2.21504501e+00,
       2.96978681e+00, 1.04168551e-02])

Here we’ve got mlog10p,ts,ns,gamma,ra_best,dec_best:

[23]:
print('TS, ns, gamma (ra,dec) = {:.2f}, {:.2f}, {:.2f} ({:.2f}, {:.2f})'.format(
    result[1], result[2], result[3], *np.degrees(result[-2:])))
TS, ns, gamma (ra,dec) = 47.00, 23.05, 2.22 (170.16, 0.60)

All-sky scans are slow#

SpatialPriorTrialRunner allows the usual get_many_fits(), etc., but in practice this is almost useless if the trials are very slow such that they require one or more cluster jobs to complete in reasonable time. Therefore, it’s more likely that you’ll want to get the trial using SpatialPriorTrialRunner and run the scans using SkyScanTrialRunner.

To get the trial, we can say:

[24]:
trial = sptr.get_one_trial(20, seed=1)
trial
[24]:
SpatialPriorTrial(evss=[[Events(136244 items | columns: dec, energy, idx, inj, log10energy, ra, sigma, sindec), Events(20 items | columns: dec, energy, idx, inj, log10energy, ra, sigma, sindec)]], n_exs=[0], ra=array([2.96978681]), dec=array([0.01041686]))

Note that this also allows us to inspect the true source position(s) selected for this trial:

[25]:
np.degrees([trial.ra, trial.dec]).ravel()
[25]:
array([170.15625   ,   0.59684183])

In the above scan, the implicit trial was the same as this one, since the random seed was the same. And sure enough, the hottest point in the sky, after accounting for the prior, was at the position of the source.

We can reproduce that result by performing the all-sky scan manually:

[26]:
sstr = cy.get_sky_scan_trial_runner(nside=64, min_dec=-dec85, max_dec=dec85)
[27]:
%time scan = sstr.get_one_scan_from_trial(trial, mp_cpus=15, logging=True)
Scanning 48984 locations using 15 cores:
      48984/48984 coordinates complete.
CPU times: user 275 ms, sys: 325 ms, total: 600 ms
Wall time: 58.7 s
[28]:
scan.shape
[28]:
(4, 49152)

The 0th axis of this array has the elements mlog10p,ts,ns,gamma. The TS map looks like so:

[29]:
fig, ax = plt.subplots (subplot_kw=dict (projection='aitoff'))
sp = cy.plotting.SkyPlotter(pc_kw=dict())
mesh, cb = sp.plot_map(ax, scan[1], n_ticks=2)
kw = dict(color='.5', alpha=.5)
sp.plot_gp(ax, lw=.5, **kw)
sp.plot_gc(ax, **kw)
ax.grid(**kw)
cb.set_label(r'TS without prior')
plt.tight_layout()
_images/08_spatial_priors_31_0.png

The TS map including the prior looks like so:

[30]:
fig, ax = plt.subplots (subplot_kw=dict (projection='aitoff'))
sp = cy.plotting.SkyPlotter(pc_kw=dict())
ts_with_prior = scan[1] + sptr.llh_prior_term[0]
mesh, cb = sp.plot_map(ax, ts_with_prior, n_ticks=2)
kw = dict(color='.5', alpha=.5)
sp.plot_gp(ax, lw=.5, **kw)
sp.plot_gc(ax, **kw)
ax.grid(**kw)
cb.set_label(r'TS including prior')
plt.tight_layout()
_images/08_spatial_priors_33_0.png

Notice that sptr.llh_prior_term[0] is for the 0th prior. If we had given multiple priors, they would have higher indices in this sequence.

Now, if we disallow TS values below 0, we get:

[31]:
fig, ax = plt.subplots (subplot_kw=dict (projection='aitoff'))
sp = cy.plotting.SkyPlotter(pc_kw=dict())
mesh, cb = sp.plot_map(ax, np.maximum(0, ts_with_prior), n_ticks=2)
kw = dict(color='.5', alpha=.5)
sp.plot_gp(ax, lw=.5, **kw)
sp.plot_gc(ax, **kw)
ax.grid(**kw)
cb.set_label(r'TS including prior')
plt.tight_layout()
_images/08_spatial_priors_35_0.png

Counts vs Flux#

SpatialPriorTrialRunner supports counts \(\leftrightarrow\) flux conversions in much the same way as other trial runners. However, signal-injector-based conversions are not possible because the signal appears in a different position for each trial. So for example, the following is not possible:

[32]:
sptr.to_E2dNdE(20, E0=100, unit=1e3)  # TeV/cm2/s @ 100TeV ?
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/tmp/ipykernel_29431/582389252.py in <module>
----> 1 sptr.to_E2dNdE(20, E0=100, unit=1e3)  # TeV/cm2/s @ 100TeV ?

/data/user/jthwaites/csky/csky/trial.py in to_E2dNdE(self, ns, flux, *a, **kw)
    212             * clarify sig_inj_acc_total vs get_acc_total situation in both code and docs
    213         """
--> 214         flux, params, fluxargs, acc = self._split_params_fluxargs_acc(flux, *a, **kw)
    215         return flux.to_E2dNdE(ns, acc, **fluxargs)
    216

/data/user/jthwaites/csky/csky/trial.py in _split_params_fluxargs_acc(self, flux, *a, **kw)
    157             flux = hyp.PowerLawFlux(params['gamma'])
    158         else:
--> 159             fluxs = np.atleast_1d(self.sig_injs[-1].flux)
    160             if not np.all(fluxs == fluxs[0]):
    161                 raise ValueError('cannot convert to mixed fluxes')

AttributeError: 'SpatialPriorTrialRunner' object has no attribute 'sig_injs'

But this is fine:

[33]:
sptr.to_E2dNdE(20, E0=100, unit=1e3, gamma=2)  # TeV/cm2/s @ 100TeV !
[33]:
1.0685320290882531e-11

This refers to the flux that would, on average, yield 20 events. Depending on the true source position, the flux might yield more or fewer events; but averaging over the spatial prior, this flux yields 20 events.

Remarks#

Spatial prior analysis seems complex, but ultimately it just comes down to performing all-sky scans and penalizing the TS according to the prior.

Future work will include making it easy to restrict the pixel set that is evaluated and documenting how to do so. This is important for optimizing transient analyses where very few events are likely to be present, and only small time ranges need to be studied for ecah source.