Skip to content

Commit 9597ffa

Browse files
committed
Fixing energy fitting
1 parent 573345d commit 9597ffa

24 files changed

+982
-1271
lines changed

hawc_hal/HAL.py

+66-27
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ def __init__(self, name, maptree, response_file, roi, flat_sky_pixels_sizes=0.17
3232
# Store ROI
3333
self._roi = roi
3434

35+
# Set up the flat-sky projection
36+
37+
self._flat_sky_projection = roi.get_flat_sky_projection(flat_sky_pixels_sizes)
38+
3539
# Read map tree (data)
3640

3741
self._maptree = map_tree_factory(maptree, roi=roi)
@@ -65,12 +69,10 @@ def __init__(self, name, maptree, response_file, roi, flat_sky_pixels_sizes=0.17
6569
self._convolved_ext_sources = ConvolvedSourcesContainer()
6670

6771
# By default all energy/nHit bins are used
68-
self._all_planes = range(len(self._maptree))
69-
self._active_planes = range(len(self._maptree))
70-
71-
# Set up the flat-sky projection
72+
self._all_planes = list(self._maptree.analysis_bins_labels)
7273

73-
self._flat_sky_projection = roi.get_flat_sky_projection(flat_sky_pixels_sizes)
74+
# The active planes list always contains the list of *indexes* of the active planes
75+
self._active_planes_idx = range(len(self._maptree))
7476

7577
# Set up the transformations from the flat-sky projection to Healpix, as well as the list of active pixels
7678
# (one for each energy/nHit bin). We make a separate transformation because different energy bins might have
@@ -132,7 +134,7 @@ def _compute_likelihood_biases(self):
132134
obs = data_analysis_bin.observation_map.as_partial()
133135
bkg = data_analysis_bin.background_map.as_partial()
134136

135-
sat_model = np.maximum(obs - bkg, 1e-30).astype(np.float64)
137+
sat_model = np.clip(obs - bkg, 1e-50, None).astype(np.float64)
136138

137139
self._saturated_model_like_per_maptree[i] = log_likelihood(obs, bkg, sat_model) - this_log_factorial
138140

@@ -144,11 +146,45 @@ def get_saturated_model_likelihood(self):
144146
"""
145147
return np.sum(self._saturated_model_like_per_maptree)
146148

147-
def set_active_measurements(self, bin_id_min, bin_id_max):
149+
def set_active_measurements(self, bin_id_min=None, bin_id_max=None, bin_list=None):
150+
151+
# Check for legal input
152+
if bin_id_min is not None:
153+
154+
assert bin_id_max is not None, "If you provide a minimum bin, you also need to provide a maximum bin"
155+
156+
# Make sure they are strings
157+
bin_id_min = str(bin_id_min)
158+
bin_id_max = str(bin_id_max)
159+
160+
assert bin_id_min in self._all_planes and bin_id_max in self._all_planes, "Illegal bin_names"
161+
162+
assert bin_list is None, "You can either provide a minimum and maximum bin, or a bin list, but not both"
148163

149-
assert bin_id_min in self._all_planes and bin_id_max in self._all_planes, "Illegal bin_name numbers"
164+
idx1 = self._all_planes.index(bin_id_min)
165+
idx2 = self._all_planes.index(bin_id_max)
150166

151-
self._active_planes = range(bin_id_min, bin_id_max + 1)
167+
self._active_planes_idx = range(idx1, idx2 + 1)
168+
169+
else:
170+
171+
assert bin_id_max is None, "If you provide a maximum bin, you also need to provide a minimum bin"
172+
173+
assert bin_list is not None
174+
175+
self._active_planes_idx = []
176+
177+
for this_bin in bin_list:
178+
179+
try:
180+
181+
this_idx = self._all_planes.index(str(this_bin))
182+
183+
except ValueError:
184+
185+
raise ValueError("Bin %s it not contained in this response" % this_bin)
186+
187+
self._active_planes_idx.append(this_idx)
152188

153189
def display(self):
154190

@@ -179,7 +215,7 @@ def display(self):
179215
print("")
180216
print("Active energy/nHit planes: ")
181217
print("---------------------------\n")
182-
print(self._active_planes)
218+
print(self._active_planes_idx)
183219

184220
def set_model(self, likelihood_model_instance):
185221
"""
@@ -231,13 +267,13 @@ def display_spectrum(self):
231267
n_point_sources = self._likelihood_model.get_number_of_point_sources()
232268
n_ext_sources = self._likelihood_model.get_number_of_extended_sources()
233269

234-
total_counts = np.zeros(len(self._active_planes), dtype=float)
270+
total_counts = np.zeros(len(self._active_planes_idx), dtype=float)
235271
total_model = np.zeros_like(total_counts)
236272
model_only = np.zeros_like(total_counts)
237273
residuals = np.zeros_like(total_counts)
238274
net_counts = np.zeros_like(total_counts)
239275

240-
for i, energy_id in enumerate(self._active_planes):
276+
for i, energy_id in enumerate(self._active_planes_idx):
241277

242278
data_analysis_bin = self._maptree[energy_id]
243279

@@ -270,11 +306,11 @@ def display_spectrum(self):
270306

271307
fig, subs = plt.subplots(2, 1, gridspec_kw={'height_ratios': [2, 1], 'hspace': 0})
272308

273-
subs[0].errorbar(self._active_planes, net_counts, yerr=np.sqrt(total_counts),
309+
subs[0].errorbar(self._active_planes_idx, net_counts, yerr=np.sqrt(total_counts),
274310
capsize=0,
275311
color='black', label='Net counts', fmt='.')
276312

277-
subs[0].plot(self._active_planes, model_only, label='Convolved model')
313+
subs[0].plot(self._active_planes_idx, model_only, label='Convolved model')
278314

279315
subs[0].legend(bbox_to_anchor=(1.0, 1.0), loc="upper right",
280316
numpoints=1)
@@ -283,21 +319,21 @@ def display_spectrum(self):
283319
subs[1].axhline(0, linestyle='--')
284320

285321
subs[1].errorbar(
286-
self._active_planes, residuals,
322+
self._active_planes_idx, residuals,
287323
yerr=np.ones(residuals.shape),
288324
capsize=0, fmt='.'
289325
)
290326

291-
x_limits = [min(self._active_planes) - 0.5, max(self._active_planes) + 0.5]
327+
x_limits = [min(self._active_planes_idx) - 0.5, max(self._active_planes_idx) + 0.5]
292328

293329
subs[0].set_yscale("log", nonposy='clip')
294330
subs[0].set_ylabel("Counts per bin")
295331
subs[0].set_xticks([])
296332

297333
subs[1].set_xlabel("Analysis bin")
298334
subs[1].set_ylabel(r"$\frac{{cts - mod - bkg}}{\sqrt{mod + bkg}}$")
299-
subs[1].set_xticks(self._active_planes)
300-
subs[1].set_xticklabels(self._active_planes)
335+
subs[1].set_xticks(self._active_planes_idx)
336+
subs[1].set_xticklabels(self._active_planes_idx)
301337

302338
subs[0].set_xlim(x_limits)
303339
subs[1].set_xlim(x_limits)
@@ -323,16 +359,19 @@ def get_log_like(self):
323359

324360
for i, data_analysis_bin in enumerate(self._maptree):
325361

326-
if i not in self._active_planes:
362+
if i not in self._active_planes_idx:
327363
continue
328364

329365
this_model_map_hpx = self._get_expectation(data_analysis_bin, i, n_point_sources, n_ext_sources)
330366

331367
# Now compare with observation
332368
bkg_renorm = self._nuisance_parameters.values()[0].value
333369

334-
this_pseudo_log_like = log_likelihood(data_analysis_bin.observation_map.as_partial(),
335-
data_analysis_bin.background_map.as_partial() * bkg_renorm,
370+
obs = data_analysis_bin.observation_map.as_partial() # type: np.array
371+
bkg = data_analysis_bin.background_map.as_partial() * bkg_renorm # type: np.array
372+
373+
this_pseudo_log_like = log_likelihood(obs,
374+
bkg,
336375
this_model_map_hpx)
337376

338377
total_log_like += this_pseudo_log_like - self._log_factorials[i] - self._saturated_model_like_per_maptree[i]
@@ -364,7 +403,7 @@ def get_simulated_dataset(self, name):
364403

365404
for i, data_analysis_bin in enumerate(self._maptree):
366405

367-
if i not in self._active_planes:
406+
if i not in self._active_planes_idx:
368407

369408
expectations.append(None)
370409

@@ -389,7 +428,7 @@ def get_simulated_dataset(self, name):
389428
# Substitute the observation and background for each data analysis bin
390429
for i, (data_analysis_bin, orig_data_analysis_bin) in enumerate(zip(self._clone[0]._maptree, self._maptree)):
391430

392-
if i not in self._active_planes:
431+
if i not in self._active_planes_idx:
393432

394433
continue
395434

@@ -524,13 +563,13 @@ def display_fit(self, smoothing_kernel_sigma=0.1):
524563
# The image is going to cover the diameter plus 20% padding
525564
xsize = self._get_optimal_xsize(resolution)
526565

527-
n_active_planes = len(self._active_planes)
566+
n_active_planes = len(self._active_planes_idx)
528567

529568
fig, subs = plt.subplots(n_active_planes, 3, figsize=(8, n_active_planes * 2))
530569

531-
with progress_bar(len(self._active_planes), title='Smoothing maps') as prog_bar:
570+
with progress_bar(len(self._active_planes_idx), title='Smoothing maps') as prog_bar:
532571

533-
for i, plane_id in enumerate(self._active_planes):
572+
for i, plane_id in enumerate(self._active_planes_idx):
534573

535574
data_analysis_bin = self._maptree[plane_id]
536575

@@ -604,7 +643,7 @@ def display_stacked_image(self, smoothing_kernel_sigma=0.5):
604643
# The image is going to cover the diameter plus 20% padding
605644
xsize = self._get_optimal_xsize(resolution)
606645

607-
active_planes_bins = map(lambda x: self._maptree[x], self._active_planes)
646+
active_planes_bins = map(lambda x: self._maptree[x], self._active_planes_idx)
608647

609648
# Get the center of the projection for this plane
610649
this_ra, this_dec = self._roi.ra_dec_center

hawc_hal/convenience_functions/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from ..HAL import HAL
2+
3+
from threeML import DataList, JointLikelihood
4+
5+
6+
def fit_point_source(roi,
7+
maptree,
8+
response,
9+
point_source_model,
10+
bin_list,
11+
confidence_intervals=False,
12+
liff=False,
13+
pixel_size=0.17,
14+
verbose=False):
15+
16+
data_radius = roi.data_radius.to("deg").value
17+
18+
if not liff:
19+
20+
# This is a 3ML plugin
21+
hawc = HAL("HAWC",
22+
maptree,
23+
response,
24+
roi,
25+
flat_sky_pixels_sizes=pixel_size)
26+
27+
hawc.set_active_measurements(bin_list=bin_list)
28+
29+
else:
30+
31+
from threeML import HAWCLike
32+
33+
hawc = HAWCLike("HAWC",
34+
maptree,
35+
response,
36+
fullsky=True)
37+
38+
hawc.set_bin_list(bin_list)
39+
40+
ra_roi, dec_roi = roi.ra_dec_center
41+
42+
hawc.set_ROI(ra_roi, dec_roi, data_radius)
43+
44+
if not liff:
45+
46+
hawc.display()
47+
48+
data = DataList(hawc)
49+
50+
jl = JointLikelihood(point_source_model, data, verbose=verbose)
51+
52+
point_source_model.display(complete=True)
53+
54+
try:
55+
56+
jl.set_minimizer("minuit")
57+
58+
except:
59+
60+
jl.set_minimizer("minuit")
61+
62+
param_df, like_df = jl.fit()
63+
64+
if confidence_intervals:
65+
66+
ci = jl.get_errors()
67+
68+
else:
69+
70+
ci = None
71+
72+
return param_df, like_df, ci, jl.results

hawc_hal/convolved_source/convolved_extended_source.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def _select_with_wrap_around(arr, start, stop, wrap=(360, 0)):
88
if start > stop:
99

1010
# This can happen if for instance lon_start = 280 and lon_stop = 10, which means we
11-
# should keep between 280 and 360 and then between 360 to 10
11+
# should keep between 280 and 360 and then between 0 to 10
1212
idx = ((arr >= stop) & (arr <= wrap[0])) | ((arr >= wrap[1]) & (arr <= start))
1313

1414
else:

hawc_hal/convolved_source/convolved_point_source.py

+39-14
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import numpy as np
2+
import os
23

34
from astromodels import PointSource
45
from ..psf_fast import PSFInterpolator
5-
6+
from ..interpolation.log_log_interpolation import LogLogInterpolator
67

78
from threeML.exceptions.custom_exceptions import custom_warnings
89

@@ -47,11 +48,7 @@ def _update_dec_bins(self, dec_src):
4748
self._flat_sky_projection),
4849
self._response_energy_bins)
4950

50-
# for i, psf_i in enumerate(self._psf_interpolators):
51-
#
52-
# print("PSF %i: %.3f, %.3f" % (i, psf_i._psf.truncation_radius, psf_i._psf.kernel_radius))
53-
54-
def get_source_map(self, response_bin_id, tag=None):
51+
def get_source_map(self, response_bin_id, tag=None, integrate=False):
5552

5653
# Get current point source position
5754
# NOTE: this might change if the point source position is free during the fit,
@@ -75,14 +72,16 @@ def get_source_map(self, response_bin_id, tag=None):
7572
# is very fast
7673
this_map = psf_interpolator.point_source_image(ra_src, dec_src)
7774

78-
# Check that the point source is contained in the ROI, if not print a warning
75+
# Check that the point source image is entirely contained in the ROI, if not print a warning
7976
map_sum = this_map.sum()
77+
8078
if not np.isclose(map_sum, 1.0, rtol=1e-2):
8179

8280
custom_warnings.warn("PSF for source %s is not entirely contained "
83-
"in ROI for response bin %s. Fraction is %.2f instead of 1.0" % (self._name,
84-
response_bin_id,
85-
map_sum))
81+
"in ROI for response bin %s. Fraction is %.2f instead of 1.0. "
82+
"Consider enlarging your model ROI." % (self._name,
83+
response_bin_id,
84+
map_sum))
8685

8786
# Compute the fluxes from the spectral function at the same energies as the simulated function
8887
energy_centers_keV = response_energy_bin.sim_energy_bin_centers * 1e9 # astromodels expects energies in keV
@@ -92,11 +91,37 @@ def get_source_map(self, response_bin_id, tag=None):
9291

9392
source_diff_spectrum = self._source(energy_centers_keV, tag=tag)
9493

95-
# Transform from keV^-1 cm^-2 s^-1 to TeV^-1 cm^-2 s^-1
96-
source_diff_spectrum *= 1e9
94+
# This provide a way to force the use of integration, for testing
95+
if os.environ.get("HAL_INTEGRATE_POINT_SOURCE", '').lower() == 'yes': # pragma: no cover
96+
97+
integrate = True
98+
99+
if integrate: # pragma: no cover
100+
101+
# Slower approach, where we integrate the spectra of both the simulation and the source
102+
# It is off by default because it is slower and it doesn't provide any improvement in accuracy
103+
# according to my simulations (GV)
104+
105+
interp_spectrum = LogLogInterpolator(response_energy_bin.sim_energy_bin_centers,
106+
source_diff_spectrum * 1e9,
107+
k=2)
108+
109+
interp_sim_spectrum = LogLogInterpolator(response_energy_bin.sim_energy_bin_centers,
110+
response_energy_bin.sim_differential_photon_fluxes,
111+
k=2)
112+
113+
src_spectrum = [interp_spectrum.integral(a, b) for (a, b) in zip(response_energy_bin.sim_energy_bin_low,
114+
response_energy_bin.sim_energy_bin_hi)]
115+
116+
sim_spectrum = [interp_sim_spectrum.integral(a, b) for (a, b) in zip(response_energy_bin.sim_energy_bin_low,
117+
response_energy_bin.sim_energy_bin_hi)]
118+
119+
scale = np.array(src_spectrum) / np.array(sim_spectrum)
120+
121+
else:
97122

98-
# Re-weight the detected counts
99-
scale = source_diff_spectrum / response_energy_bin.sim_differential_photon_fluxes
123+
# Transform from keV^-1 cm^-2 s^-1 to TeV^-1 cm^-2 s^-1 and re-weight the detected counts
124+
scale = source_diff_spectrum / response_energy_bin.sim_differential_photon_fluxes * 1e9
100125

101126
# Now return the map multiplied by the scale factor
102127
return np.sum(scale * response_energy_bin.sim_signal_events_per_bin) * this_map

0 commit comments

Comments
 (0)