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 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