Quickstart#
Note
This page presents the full workflow of tilepy, from the localization map to the observation schedule: display the sky map, extract the 90% credible region and its pixels, then run the scheduling. The companion notebook (button above) follows exactly these steps and can be run directly.
What you will see
What a HEALPix sky map looks like and how to display it.
How to extract the 90% probability and which pixels make it up.
How tilepy turns this region into a schedule of pointings.
The sky map#
A localization map is a HEALPix map: the sky is tiled into pixels of equal
area and each pixel i carries the probability that the source lies inside it.
tilepy reads these maps internally (module
tilepy.include.MapManagement), but to clearly visualize the starting point
one can display it with ligo.skymap.
Tip
The figure above is produced by this snippet when the docs are built (it
downloads only the small GW190728 skymap, a few MB). read_sky_map returns
a flattened map (a single NSIDE); recent IGWN alerts are often
multi-resolution (MOC): in that case use read_sky_map(url, moc=True).
tilepy handles both formats automatically.
HEALPix resolution and order#
HEALPix is hierarchical: the sphere starts as 12 base pixels (order \(\ell = 0\)) and each pixel is recursively split into 4. The order (or level) \(\ell\) therefore sets the resolution:
Every pixel covers the same solid angle:
Worked example (\(\ell = 6\)): \(\mathrm{NSIDE} = 2^6 = 64\), \(N_\mathrm{pix} = 12\cdot64^2 = 49152\), \(\Omega_\mathrm{pix} = 41253/49152 \approx 0.84\ \mathrm{deg}^2\), \(\theta_\mathrm{pix} \approx 58.6/64 \approx 0.92°\).
\(\ell\) |
NSIDE |
\(N_\mathrm{pix}\) |
Pixel area |
Pixel size |
|---|---|---|---|---|
6 |
64 |
49,152 |
\(\approx 0.84\ \mathrm{deg}^2\) |
\(\approx 0.92°\) |
8 |
256 |
786,432 |
\(\approx 0.052\ \mathrm{deg}^2\) |
\(\approx 0.23°\) |
9 |
512 |
3,145,728 |
\(\approx 0.013\ \mathrm{deg}^2\) |
\(\approx 0.11°\) |
Going from \(\ell\) to \(\ell+1\) multiplies the number of pixels by 4 and halves the pixel size. This is the multi-resolution “tree” used by IGWN MOC maps: well-localized regions are stored at high \(\ell\), empty regions at low \(\ell\). The figure below (rendered at build time) shows the same sphere pixelized at two orders:
import numpy as np, healpy as hp
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(8, 3.2))
for i, ns in enumerate([2, 8]):
hp.mollview(np.arange(hp.nside2npix(ns)), sub=(1, 2, i + 1), cbar=False,
cmap="tab20",
title=f"l={int(np.log2(ns))} (NSIDE={ns}, Npix={hp.nside2npix(ns)})")
hp.graticule()
Important
tilepy works with two orders, configurable in the .ini file:
HRnside(high \(\ell\)) to integrate the probability inside a field of view precisely;reducedNside(low \(\ell\)) to scan the sky quickly.
Extracting the 90% region#
Principle (greedy algorithm). Sort the pixels from most to least probable, accumulate their probability, and keep the first ones until the desired fraction is reached (here 0.9). The area is simply the number of retained pixels times the area of one pixel.
import numpy as np
order = np.argsort(prob)[::-1] # decreasing sort
cumul = np.cumsum(prob[order]) # cumulative sum
pix_90 = order[cumul <= 0.90] # pixels of the 90%
pix_area = hp.nside2pixarea(nside, degrees=True)
area_90 = len(pix_90) * pix_area
print(f"{len(pix_90)} pixels -> {area_90:.1f} deg²")
Once the map is loaded into a
SkyMap object, the same
computation is directly available:
skymap.getArea(0.9) # area of the 90% region (deg²)
skymap.getPixIdArea(0.9) # indices of the pixels in the region
And to retrieve the (RA, Dec) coordinates on a reduced grid — this is the list of positions the algorithm then scans:
from tilepy.include.PointingTools import GetRegionPixReduced
ra, dec, area = GetRegionPixReduced(prob, 0.9, reducedNside)
See also
The functions GetRegionPixReduced() and
getPixIdArea() implement this
sort/accumulate step. It is the same “cumulative sum of sorted pixels” scheme
found in most multi-messenger follow-up tools.
The scheduling process#
tilepy is driven by a .ini configuration file (telescope, visibility
constraints, strategy) and an
ObservationParameters object. We stay
here on the simplest case: algorithm = 2D (probability integration) for a
ground-based telescope.
Important
In 2D mode tilepy does not read any galaxy catalog, so there is
nothing heavy to download (the GLADE+ catalog Gladeplus.h5 is only
needed in 3D mode). Pass None for both the dataset directory and the
catalog name.
Minimal configuration file (LST, 2D)
[observatory]
name = LST
lat = 28.75
lon = -17.5
height = 2200
base = ground
[visibility]
sundown = -18
moondown = -0.5
earthdown = 0
moongrey = -0.5
gmoonphase = 60
minmoonsourceseparation = 30
maxmoonsourceseparation = 150
SAAThreshold = 0
[operations]
maxzenith = 60
fov = 2.0
maxRuns = 20
maxNights = 1
duration = 30
minduration = 10
useGreytime = False
minSlewing = 0
shape = circle
numberSides = 0
FoVRotation = 0
[tiling]
locCut = 99999
minimumprobcutforcatalogue = 0.01
minProbcut = 0.01
distcut = 0.5
doPlot = True
secondRound = False
zenithWeighting = 0.75
percentageMOC = 0.9
reducedNside = 64
hrnside = 512
mangrove = False
algorithm = 2D
strategy = integrated
doRank = False
countPrevious = False
alphaR = 0
betaR = 0
[general]
downloadMaxRetry = 1
downloadWaitPeriodRetry = 20
Run the computation
import datetime
from tilepy.include.CampaignDefinition import ObservationParameters
from tilepy.include.ObservationScheduler import GetSchedule
obsTime = datetime.datetime.fromisoformat("2019-07-28 08:30:00")
obspar = ObservationParameters()
# datasetDir = None, galcatName = None: 2D needs no galaxy catalog
obspar.add_parsed_args(url, obsTime, None, None,
"./output", None, None, "GW190728")
obspar.from_configfile("FollowupParameters_LST.ini")
GetSchedule(obspar)
tilepy can build a simulated Gaussian map in memory with
set_gaussian_source() — same
scheduling, zero network access.
from tilepy.include.CampaignDefinition import (
ObservationParameters, set_gaussian_source)
from tilepy.include.ObservationScheduler import GetSchedule
obspar = ObservationParameters()
obspar.add_parsed_args(None, obsTime, None, None,
"./output", None, None, "sim_event")
set_gaussian_source(obspar, ra=240.0, dec=20.0, sigma=5.0)
obspar.nside = 256
obspar.from_configfile("FollowupParameters_LST.ini")
GetSchedule(obspar)
What does GetSchedule do in 2D mode?
reads the map and computes the 90% region (previous section);
splits the night into observable dark-time windows at the site;
at each window, centers the field of view on the pixel with the highest probability still visible, adds up the probability contained in the FoV, then masks those pixels so they are not counted again;
repeats until
maxRunspointings (or until the probability aboveminProbcutis exhausted).
The result is a table of pointings
SuggestedPointings_2DProbOptimisation.txt (columns Time[UTC],
RA[deg], DEC[deg], PGW, …), together with the figures if
doPlot = True.
Reading the produced schedule#
from astropy.table import Table
res = Table.read(
"output/GW190728/PGinFoV/SuggestedPointings_2DProbOptimisation.txt",
format="ascii")
print(res)
print("Total covered probability:", res["PGW"].sum())