polpy 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- polpy/__init__.py +0 -0
- polpy/polarizationlike.py +571 -0
- polpy/poldata.py +197 -0
- polpy/polresponse.py +123 -0
- polpy-0.0.0.dist-info/LICENSE +674 -0
- polpy-0.0.0.dist-info/METADATA +23 -0
- polpy-0.0.0.dist-info/RECORD +8 -0
- polpy-0.0.0.dist-info/WHEEL +4 -0
polpy/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import numpy as np
|
|
6
|
+
import numba as nb
|
|
7
|
+
|
|
8
|
+
from astromodels import Parameter, Uniform_prior
|
|
9
|
+
from polpy.polresponse import PolResponse
|
|
10
|
+
from threeML import PluginPrototype
|
|
11
|
+
from threeML.io.plotting.step_plot import step_plot
|
|
12
|
+
from threeML.utils.binner import Rebinner
|
|
13
|
+
from threeML.utils.polarization.binned_polarization import \
|
|
14
|
+
BinnedModulationCurve
|
|
15
|
+
from threeML.utils.statistics.likelihood_functions import (
|
|
16
|
+
poisson_observed_gaussian_background, poisson_observed_poisson_background)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PolarizationLike(PluginPrototype):
|
|
20
|
+
"""
|
|
21
|
+
Preliminary POLAR polarization plugin
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, name, observation, background, response, interval_number=None, verbose=False):
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
The Polarization likelihood for POLAR. This plugin is heavily modeled off
|
|
28
|
+
the 3ML dispersion based plugins. It interpolates the spectral photon model
|
|
29
|
+
over the scattering angle bins to allow for spectral + polarization analysis.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
:param interval_number: The time interval starting from 1.
|
|
34
|
+
:param name: The name of the plugin
|
|
35
|
+
:param observation: The POLAR observation file
|
|
36
|
+
:param background: The POLAR background file
|
|
37
|
+
:param response: The POLAR polarization response
|
|
38
|
+
|
|
39
|
+
:param verbose:
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
# attach the required variables
|
|
43
|
+
|
|
44
|
+
self._observation = observation
|
|
45
|
+
self._background = background
|
|
46
|
+
|
|
47
|
+
self._observed_counts = observation.counts.astype(np.int64)
|
|
48
|
+
self._background_counts = background.counts
|
|
49
|
+
self._background_count_errors = background.count_errors
|
|
50
|
+
self._scale = observation.exposure / background.exposure
|
|
51
|
+
self._exposure = observation.exposure
|
|
52
|
+
self._background_exposure = background.exposure
|
|
53
|
+
|
|
54
|
+
self._likelihood_model = None
|
|
55
|
+
self._rebinner = None
|
|
56
|
+
|
|
57
|
+
# now do some double checks
|
|
58
|
+
|
|
59
|
+
assert len(self._observed_counts) == len(self._background_counts)
|
|
60
|
+
|
|
61
|
+
self._n_synthetic_datasets = 0
|
|
62
|
+
|
|
63
|
+
# set up the effective area correction
|
|
64
|
+
|
|
65
|
+
self._nuisance_parameter = Parameter(
|
|
66
|
+
"cons_%s" % name,
|
|
67
|
+
1.0,
|
|
68
|
+
min_value=0.8,
|
|
69
|
+
max_value=1.2,
|
|
70
|
+
delta=0.05,
|
|
71
|
+
free=False,
|
|
72
|
+
desc="Effective area correction for %s" % name)
|
|
73
|
+
|
|
74
|
+
nuisance_parameters = collections.OrderedDict()
|
|
75
|
+
nuisance_parameters[self._nuisance_parameter.name] = self._nuisance_parameter
|
|
76
|
+
|
|
77
|
+
# pass to the plugin proto
|
|
78
|
+
|
|
79
|
+
super(PolarizationLike, self).__init__(name, nuisance_parameters)
|
|
80
|
+
|
|
81
|
+
# The following vectors are the ones that will be really used for the computation. At the beginning they just
|
|
82
|
+
# point to the original ones, but if a rebinner is used and/or a mask is created through set_active_measurements,
|
|
83
|
+
# they will contain the rebinned and/or masked versions
|
|
84
|
+
|
|
85
|
+
self._current_observed_counts = self._observed_counts
|
|
86
|
+
self._current_background_counts = self._background_counts
|
|
87
|
+
self._current_background_count_errors = self._background_count_errors
|
|
88
|
+
|
|
89
|
+
self._verbose = verbose
|
|
90
|
+
|
|
91
|
+
# we can either attach or build a response
|
|
92
|
+
|
|
93
|
+
assert isinstance(response, str) or isinstance(
|
|
94
|
+
response, PolResponse), 'The response must be a file name or a PolarResponse'
|
|
95
|
+
|
|
96
|
+
if isinstance(response, PolResponse):
|
|
97
|
+
|
|
98
|
+
self._response = response
|
|
99
|
+
|
|
100
|
+
else:
|
|
101
|
+
|
|
102
|
+
self._response = PolResponse(response)
|
|
103
|
+
|
|
104
|
+
# attach the interpolators to the
|
|
105
|
+
|
|
106
|
+
self._all_interp = self._response.interpolators
|
|
107
|
+
|
|
108
|
+
# we also make sure the lengths match up here
|
|
109
|
+
assert self._response.n_scattering_bins == len(
|
|
110
|
+
self._observation.counts), 'observation counts shape does not agree with response shape'
|
|
111
|
+
|
|
112
|
+
def use_effective_area_correction(self, lower=0.5, upper=1.5):
|
|
113
|
+
"""
|
|
114
|
+
Use an area constant to correct for response issues
|
|
115
|
+
|
|
116
|
+
:param lower:
|
|
117
|
+
:param upper:
|
|
118
|
+
:return:
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
self._nuisance_parameter.free = True
|
|
122
|
+
self._nuisance_parameter.bounds = (lower, upper)
|
|
123
|
+
self._nuisance_parameter.prior = Uniform_prior(
|
|
124
|
+
lower_bound=lower, upper_bound=upper)
|
|
125
|
+
if self._verbose:
|
|
126
|
+
print('Using effective area correction')
|
|
127
|
+
|
|
128
|
+
def fix_effective_area_correction(self, value=1):
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
fix the effective area correction to a particular values
|
|
132
|
+
|
|
133
|
+
:param value:
|
|
134
|
+
:return:
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
# allow the value to be outside the bounds
|
|
138
|
+
if self._nuisance_parameter.max_value < value:
|
|
139
|
+
|
|
140
|
+
self._nuisance_parameter.max_value = value + 0.1
|
|
141
|
+
|
|
142
|
+
elif self._nuisance_parameter.min_value > value:
|
|
143
|
+
|
|
144
|
+
self._nuisance_parameter.min_value = value = 0.1
|
|
145
|
+
|
|
146
|
+
self._nuisance_parameter.fix = True
|
|
147
|
+
self._nuisance_parameter.value = value
|
|
148
|
+
|
|
149
|
+
if self._verbose:
|
|
150
|
+
print('Fixing effective area correction')
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def effective_area_correction(self):
|
|
154
|
+
|
|
155
|
+
return self._nuisance_parameter
|
|
156
|
+
|
|
157
|
+
def set_model(self, likelihood_model_instance):
|
|
158
|
+
"""
|
|
159
|
+
Set the model to be used in the joint minimization. Must be a LikelihoodModel instance.
|
|
160
|
+
:param likelihood_model_instance: instance of Model
|
|
161
|
+
:type likelihood_model_instance: astromodels.Model
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
if likelihood_model_instance is None:
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# if self._source_name is not None:
|
|
168
|
+
|
|
169
|
+
# # Make sure that the source is in the model
|
|
170
|
+
# assert self._source_name in likelihood_model_instance.sources, \
|
|
171
|
+
# "This XYLike plugin refers to the source %s, " \
|
|
172
|
+
# "but that source is not in the likelihood model" % (self._source_name)
|
|
173
|
+
|
|
174
|
+
for k, v in likelihood_model_instance.free_parameters.items():
|
|
175
|
+
|
|
176
|
+
if 'polarization.degree' in k:
|
|
177
|
+
self._pol_degree = v
|
|
178
|
+
|
|
179
|
+
if 'polarization.angle' in k:
|
|
180
|
+
self._pol_angle = v
|
|
181
|
+
|
|
182
|
+
# now we need to get the integral flux
|
|
183
|
+
|
|
184
|
+
_, integral = self._get_diff_flux_and_integral(
|
|
185
|
+
likelihood_model_instance)
|
|
186
|
+
|
|
187
|
+
self._integral_flux = integral
|
|
188
|
+
|
|
189
|
+
self._likelihood_model = likelihood_model_instance
|
|
190
|
+
|
|
191
|
+
def _get_diff_flux_and_integral(self, likelihood_model):
|
|
192
|
+
|
|
193
|
+
n_point_sources = likelihood_model.get_number_of_point_sources()
|
|
194
|
+
|
|
195
|
+
# Make a function which will stack all point sources (OGIP do not support spatial dimension)
|
|
196
|
+
|
|
197
|
+
def differential_flux(scattering_edges):
|
|
198
|
+
fluxes = likelihood_model.get_point_source_fluxes(
|
|
199
|
+
0, scattering_edges, tag=self._tag)
|
|
200
|
+
|
|
201
|
+
# If we have only one point source, this will never be executed
|
|
202
|
+
for i in range(1, n_point_sources):
|
|
203
|
+
fluxes += likelihood_model.get_point_source_fluxes(
|
|
204
|
+
i, scattering_edges, tag=self._tag)
|
|
205
|
+
|
|
206
|
+
return fluxes
|
|
207
|
+
|
|
208
|
+
# The following integrates the diffFlux function using Simpson's rule
|
|
209
|
+
# This assume that the intervals e1,e2 are all small, which is guaranteed
|
|
210
|
+
# for any reasonable response matrix, given that e1 and e2 are Monte-Carlo
|
|
211
|
+
# scattering_edges. It also assumes that the function is smooth in the interval
|
|
212
|
+
# e1 - e2 and twice-differentiable, again reasonable on small intervals for
|
|
213
|
+
# decent models. It might fail for models with too sharp features, smaller
|
|
214
|
+
# than the size of the monte carlo interval.
|
|
215
|
+
|
|
216
|
+
def integral(e1, e2):
|
|
217
|
+
# Simpson's rule
|
|
218
|
+
|
|
219
|
+
return (e2 - e1) / 6.0 * (differential_flux(e1) + 4 * differential_flux(
|
|
220
|
+
(e1 + e2) / 2.0) + differential_flux(e2))
|
|
221
|
+
|
|
222
|
+
return differential_flux, integral
|
|
223
|
+
|
|
224
|
+
def _get_model_rate(self):
|
|
225
|
+
|
|
226
|
+
# first we need to get the integrated expectation from the spectrum
|
|
227
|
+
|
|
228
|
+
intergal_spectrum = np.array(
|
|
229
|
+
[self._integral_flux(emin, emax) for emin, emax in zip(self._response.ene_lo, self._response.ene_hi)])
|
|
230
|
+
|
|
231
|
+
# we evaluate at the center of the bin. the bin widths are already included
|
|
232
|
+
eval_points = np.array(
|
|
233
|
+
[[ene, self._pol_angle.value, self._pol_degree.value] for ene in self._response.energy_mid])
|
|
234
|
+
|
|
235
|
+
# expectation = []
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# # create the model counts by summing over energy
|
|
239
|
+
|
|
240
|
+
# for i, interpolator in enumerate(self._all_interp):
|
|
241
|
+
# rate = np.dot(interpolator(eval_points), intergal_spectrum)
|
|
242
|
+
|
|
243
|
+
# expectation.append(rate)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
return _interpolate_all(self._all_interp, intergal_spectrum, eval_points)
|
|
247
|
+
|
|
248
|
+
def _get_model_counts(self):
|
|
249
|
+
|
|
250
|
+
if self._rebinner is None:
|
|
251
|
+
model_rate = self._get_model_rate()
|
|
252
|
+
|
|
253
|
+
else:
|
|
254
|
+
|
|
255
|
+
model_rate, = self._rebinner.rebin(self._get_model_rate())
|
|
256
|
+
|
|
257
|
+
return self._nuisance_parameter.value * self._exposure * model_rate
|
|
258
|
+
|
|
259
|
+
def get_log_like(self):
|
|
260
|
+
|
|
261
|
+
model_counts = self._get_model_counts()
|
|
262
|
+
|
|
263
|
+
if self._background.is_poisson:
|
|
264
|
+
|
|
265
|
+
loglike, bkg_model = poisson_observed_poisson_background(
|
|
266
|
+
self._current_observed_counts, self._current_background_counts, self._scale, model_counts)
|
|
267
|
+
|
|
268
|
+
else:
|
|
269
|
+
|
|
270
|
+
loglike, bkg_model = poisson_observed_gaussian_background(
|
|
271
|
+
self._current_observed_counts, self._current_background_counts, self._current_background_count_errors,
|
|
272
|
+
model_counts)
|
|
273
|
+
|
|
274
|
+
return np.sum(loglike)
|
|
275
|
+
|
|
276
|
+
def inner_fit(self):
|
|
277
|
+
|
|
278
|
+
return self.get_log_like()
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def scattering_boundaries(self):
|
|
282
|
+
"""
|
|
283
|
+
Energy boundaries of channels currently in use (rebinned, if a rebinner is active)
|
|
284
|
+
|
|
285
|
+
:return: (sa_min, sa_max)
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
scattering_edges = np.array(self._observation.edges)
|
|
289
|
+
|
|
290
|
+
sa_min, sa_max = scattering_edges[:-1], scattering_edges[1:]
|
|
291
|
+
|
|
292
|
+
if self._rebinner is not None:
|
|
293
|
+
# Get the rebinned chans. NOTE: these are already masked
|
|
294
|
+
|
|
295
|
+
sa_min, sa_max = self._rebinner.get_new_start_and_stop(
|
|
296
|
+
sa_min, sa_max)
|
|
297
|
+
|
|
298
|
+
return sa_min, sa_max
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def bin_widths(self):
|
|
302
|
+
|
|
303
|
+
sa_min, sa_max = self.scattering_boundaries
|
|
304
|
+
|
|
305
|
+
return sa_max - sa_min
|
|
306
|
+
|
|
307
|
+
def display(self,
|
|
308
|
+
ax=None,
|
|
309
|
+
show_data=True,
|
|
310
|
+
show_model=True,
|
|
311
|
+
show_total=False,
|
|
312
|
+
model_kwargs={},
|
|
313
|
+
data_kwargs={},
|
|
314
|
+
edges=True,
|
|
315
|
+
min_rate=None):
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
Display the data, model, or both.
|
|
319
|
+
|
|
320
|
+
:param ax:
|
|
321
|
+
:param show_data:
|
|
322
|
+
:param show_model:
|
|
323
|
+
:param show_total:
|
|
324
|
+
:param model_kwargs:
|
|
325
|
+
:param data_kwargs:
|
|
326
|
+
:return:
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
tmp = ((self._observed_counts / self._exposure) -
|
|
330
|
+
self._background_counts / self._background_exposure)
|
|
331
|
+
|
|
332
|
+
scattering_edges = np.array(self._observation.edges)
|
|
333
|
+
|
|
334
|
+
sa_min, sa_max = scattering_edges[:-1], scattering_edges[1:]
|
|
335
|
+
|
|
336
|
+
tmp_db = ((self._observed_counts / self._exposure) - self._background_counts / self._background_exposure) / (
|
|
337
|
+
sa_max - sa_min)
|
|
338
|
+
|
|
339
|
+
old_rebinner = self._rebinner
|
|
340
|
+
|
|
341
|
+
if min_rate is not None:
|
|
342
|
+
|
|
343
|
+
rebinner = Rebinner(tmp_db, min_rate, mask=None)
|
|
344
|
+
|
|
345
|
+
self._apply_rebinner(rebinner)
|
|
346
|
+
|
|
347
|
+
net_rate = rebinner.rebin(tmp)
|
|
348
|
+
else:
|
|
349
|
+
|
|
350
|
+
net_rate = tmp
|
|
351
|
+
|
|
352
|
+
sa_min, sa_max = self.scattering_boundaries
|
|
353
|
+
|
|
354
|
+
if show_total:
|
|
355
|
+
show_model = False
|
|
356
|
+
show_data = False
|
|
357
|
+
|
|
358
|
+
if ax is None:
|
|
359
|
+
|
|
360
|
+
fig, ax = plt.subplots()
|
|
361
|
+
|
|
362
|
+
else:
|
|
363
|
+
|
|
364
|
+
fig = ax.get_figure()
|
|
365
|
+
|
|
366
|
+
xs = self.scattering_boundaries
|
|
367
|
+
|
|
368
|
+
if show_total:
|
|
369
|
+
|
|
370
|
+
total_rate = self._current_observed_counts / self._exposure / self.bin_widths
|
|
371
|
+
|
|
372
|
+
bkg_rate = self._current_background_counts / \
|
|
373
|
+
self._background_exposure / self.bin_widths
|
|
374
|
+
|
|
375
|
+
total_errors = np.sqrt(total_rate)
|
|
376
|
+
|
|
377
|
+
if self._background.is_poisson:
|
|
378
|
+
|
|
379
|
+
bkg_errors = np.sqrt(bkg_rate)
|
|
380
|
+
|
|
381
|
+
else:
|
|
382
|
+
|
|
383
|
+
bkg_errors = self._current_background_count_errors / self.bin_widths
|
|
384
|
+
|
|
385
|
+
ax.hlines(total_rate, sa_min, sa_max,
|
|
386
|
+
color='#7D0505', **data_kwargs)
|
|
387
|
+
ax.vlines(
|
|
388
|
+
np.mean([xs], axis=1),
|
|
389
|
+
total_rate - total_errors,
|
|
390
|
+
total_rate + total_errors,
|
|
391
|
+
color='#7D0505',
|
|
392
|
+
**data_kwargs)
|
|
393
|
+
|
|
394
|
+
ax.hlines(bkg_rate, sa_min, sa_max, color='#0D5BAE', **data_kwargs)
|
|
395
|
+
ax.vlines(
|
|
396
|
+
np.mean([xs], axis=1), bkg_rate - bkg_errors, bkg_rate + bkg_errors, color='#0D5BAE', **data_kwargs)
|
|
397
|
+
|
|
398
|
+
if show_data:
|
|
399
|
+
|
|
400
|
+
if self._background.is_poisson:
|
|
401
|
+
|
|
402
|
+
errors = np.sqrt((self._current_observed_counts / self._exposure**2 / self.bin_widths**2) +
|
|
403
|
+
(self._current_background_counts / self._background_exposure**2/ self.bin_widths**2))
|
|
404
|
+
|
|
405
|
+
else:
|
|
406
|
+
|
|
407
|
+
errors = np.sqrt((self._current_observed_counts / self._exposure**2 / self.bin_widths**2) +
|
|
408
|
+
(self._current_background_count_errors / self._background_exposure/ self.bin_widths)**2)
|
|
409
|
+
|
|
410
|
+
ax.hlines(net_rate / self.bin_widths,
|
|
411
|
+
sa_min, sa_max, **data_kwargs)
|
|
412
|
+
ax.vlines(
|
|
413
|
+
np.mean([xs], axis=1), (net_rate - errors) /
|
|
414
|
+
self.bin_widths, (net_rate + errors) / self.bin_widths,
|
|
415
|
+
**data_kwargs)
|
|
416
|
+
|
|
417
|
+
if show_model:
|
|
418
|
+
|
|
419
|
+
if edges:
|
|
420
|
+
|
|
421
|
+
step_plot(
|
|
422
|
+
ax=ax,
|
|
423
|
+
xbins=np.vstack([sa_min, sa_max]).T,
|
|
424
|
+
y=self._get_model_counts() / self._exposure / self.bin_widths,
|
|
425
|
+
**model_kwargs)
|
|
426
|
+
|
|
427
|
+
else:
|
|
428
|
+
|
|
429
|
+
y = self._get_model_counts() / self._exposure / self.bin_widths
|
|
430
|
+
ax.hlines(y, sa_min, sa_max, **model_kwargs)
|
|
431
|
+
|
|
432
|
+
ax.set_xlabel('Scattering Angle')
|
|
433
|
+
ax.set_ylabel('Net Rate (cnt/s/bin)')
|
|
434
|
+
|
|
435
|
+
if old_rebinner is not None:
|
|
436
|
+
|
|
437
|
+
# There was a rebinner, use it. Note that the rebinner applies the mask by itself
|
|
438
|
+
|
|
439
|
+
self._apply_rebinner(old_rebinner)
|
|
440
|
+
|
|
441
|
+
else:
|
|
442
|
+
|
|
443
|
+
self.remove_rebinning()
|
|
444
|
+
|
|
445
|
+
return fig
|
|
446
|
+
|
|
447
|
+
@property
|
|
448
|
+
def observation(self):
|
|
449
|
+
return self._observation
|
|
450
|
+
|
|
451
|
+
@property
|
|
452
|
+
def background(self):
|
|
453
|
+
return self._background
|
|
454
|
+
|
|
455
|
+
@contextmanager
|
|
456
|
+
def _without_rebinner(self):
|
|
457
|
+
|
|
458
|
+
# Store rebinner for later use
|
|
459
|
+
|
|
460
|
+
rebinner = self._rebinner
|
|
461
|
+
|
|
462
|
+
# Clean mask and rebinning
|
|
463
|
+
|
|
464
|
+
self.remove_rebinning()
|
|
465
|
+
|
|
466
|
+
# Execute whathever
|
|
467
|
+
|
|
468
|
+
yield
|
|
469
|
+
|
|
470
|
+
# Restore mask and rebinner (if any)
|
|
471
|
+
|
|
472
|
+
if rebinner is not None:
|
|
473
|
+
|
|
474
|
+
# There was a rebinner, use it. Note that the rebinner applies the mask by itself
|
|
475
|
+
|
|
476
|
+
self._apply_rebinner(rebinner)
|
|
477
|
+
|
|
478
|
+
def rebin_on_background(self, min_number_of_counts):
|
|
479
|
+
"""
|
|
480
|
+
Rebin the spectrum guaranteeing the provided minimum number of counts in each background bin. This is usually
|
|
481
|
+
required for spectra with very few background counts to make the Poisson profile likelihood meaningful.
|
|
482
|
+
Of course this is not relevant if you treat the background as ideal, nor if the background spectrum has
|
|
483
|
+
Gaussian errors.
|
|
484
|
+
|
|
485
|
+
The observed spectrum will be rebinned in the same fashion as the background spectrum.
|
|
486
|
+
|
|
487
|
+
To neutralize this completely, use "remove_rebinning"
|
|
488
|
+
|
|
489
|
+
:param min_number_of_counts: the minimum number of counts in each bin
|
|
490
|
+
:return: none
|
|
491
|
+
"""
|
|
492
|
+
|
|
493
|
+
# NOTE: the rebinner takes care of the mask already
|
|
494
|
+
|
|
495
|
+
assert self._background is not None, "This data has no background, cannot rebin on background!"
|
|
496
|
+
|
|
497
|
+
rebinner = Rebinner(self._background_counts,
|
|
498
|
+
min_number_of_counts, mask=None)
|
|
499
|
+
|
|
500
|
+
self._apply_rebinner(rebinner)
|
|
501
|
+
|
|
502
|
+
def rebin_on_source(self, min_number_of_counts):
|
|
503
|
+
"""
|
|
504
|
+
Rebin the spectrum guaranteeing the provided minimum number of counts in each source bin.
|
|
505
|
+
|
|
506
|
+
To neutralize this completely, use "remove_rebinning"
|
|
507
|
+
|
|
508
|
+
:param min_number_of_counts: the minimum number of counts in each bin
|
|
509
|
+
:return: none
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
# NOTE: the rebinner takes care of the mask already
|
|
513
|
+
|
|
514
|
+
rebinner = Rebinner(self._observed_counts,
|
|
515
|
+
min_number_of_counts, mask=None)
|
|
516
|
+
|
|
517
|
+
self._apply_rebinner(rebinner)
|
|
518
|
+
|
|
519
|
+
def _apply_rebinner(self, rebinner):
|
|
520
|
+
|
|
521
|
+
self._rebinner = rebinner
|
|
522
|
+
|
|
523
|
+
# Apply the rebinning to everything.
|
|
524
|
+
# NOTE: the output of the .rebin method are the vectors with the mask *already applied*
|
|
525
|
+
|
|
526
|
+
self._current_observed_counts, = self._rebinner.rebin(
|
|
527
|
+
self._observed_counts)
|
|
528
|
+
|
|
529
|
+
if self._background is not None:
|
|
530
|
+
|
|
531
|
+
self._current_background_counts, = self._rebinner.rebin(
|
|
532
|
+
self._background_counts)
|
|
533
|
+
|
|
534
|
+
if self._background_count_errors is not None:
|
|
535
|
+
# NOTE: the output of the .rebin method are the vectors with the mask *already applied*
|
|
536
|
+
|
|
537
|
+
self._current_background_count_errors, = self._rebinner.rebin_errors(
|
|
538
|
+
self._background_count_errors)
|
|
539
|
+
|
|
540
|
+
if self._verbose:
|
|
541
|
+
print("Now using %s bins" % self._rebinner.n_bins)
|
|
542
|
+
|
|
543
|
+
def remove_rebinning(self):
|
|
544
|
+
"""
|
|
545
|
+
Remove the rebinning scheme set with rebin_on_background.
|
|
546
|
+
|
|
547
|
+
:return:
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
self._rebinner = None
|
|
551
|
+
|
|
552
|
+
self._current_observed_counts = self._observed_counts
|
|
553
|
+
self._current_background_counts = self._background_counts
|
|
554
|
+
self._current_background_count_errors = self._background_count_errors
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
@nb.njit(fastmath=True)
|
|
560
|
+
def _interpolate_all(interpolators, integral_spectrum, eval_points):
|
|
561
|
+
|
|
562
|
+
N = len(interpolators)
|
|
563
|
+
expectation = np.empty(N)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
for n in range(N):
|
|
568
|
+
|
|
569
|
+
expectation[n] = np.dot(interpolators[n].evaluate(eval_points), integral_spectrum )
|
|
570
|
+
|
|
571
|
+
return expectation
|