Passed
Push — master ( bc1462...77290c )
by Giacomo
11:38
created

hawc_hal.HAL.HAL._write_a_map()   B

Complexity

Conditions 7

Size

Total Lines 49
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 31
dl 0
loc 49
rs 7.736
c 0
b 0
f 0
cc 7
nop 5
1
from __future__ import print_function
0 ignored issues
show
Coding Style Naming introduced by
Module name "HAL" doesn't conform to snake_case naming style ('([a-z_][a-z0-9_]*)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
Coding Style introduced by
This module should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
2
3
import copy
4
import collections
5
6
import numpy as np
7
import healpy as hp
8
import matplotlib.pyplot as plt
9
from scipy.stats import poisson
10
11
from astropy.convolution import Gaussian2DKernel
12
from astropy.convolution import convolve_fft as convolve
13
14
from threeML.plugin_prototype import PluginPrototype
15
from threeML.plugins.gammaln import logfactorial
16
from threeML.parallel import parallel_client
17
from threeML.io.progress_bar import progress_bar
18
19
from astromodels import Parameter
20
21
from hawc_hal.maptree import map_tree_factory
22
from hawc_hal.maptree.map_tree import MapTree
23
from hawc_hal.maptree.data_analysis_bin import DataAnalysisBin
24
from hawc_hal.response import hawc_response_factory
25
from hawc_hal.convolved_source import ConvolvedPointSource, \
26
    ConvolvedExtendedSource3D, ConvolvedExtendedSource2D, ConvolvedSourcesContainer
27
from hawc_hal.healpix_handling import FlatSkyToHealpixTransform
28
from hawc_hal.healpix_handling import SparseHealpix
29
from hawc_hal.healpix_handling import get_gnomonic_projection
30
from hawc_hal.psf_fast import PSFConvolutor
31
from hawc_hal.log_likelihood import log_likelihood
32
from hawc_hal.util import ra_to_longitude
33
34
35
36
class HAL(PluginPrototype):
0 ignored issues
show
best-practice introduced by
Too many instance attributes (17/7)
Loading history...
37
    """
38
    The HAWC Accelerated Likelihood plugin for 3ML.
39
    :param name: name for the plugin
40
    :param maptree: Map Tree (either ROOT or hdf5 format)
41
    :param response: Response of HAWC (either ROOT or hd5 format)
42
    :param roi: a ROI instance describing the Region Of Interest
43
    :param flat_sky_pixels_size: size of the pixel for the flat sky projection (Hammer Aitoff)
44
    """
45
46
    def __init__(self, name, maptree, response_file, roi, flat_sky_pixels_size=0.17):
0 ignored issues
show
best-practice introduced by
Too many arguments (6/5)
Loading history...
47
48
        # Store ROI
49
        self._roi = roi
50
51
        # Set up the flat-sky projection
52
53
        self._flat_sky_projection = roi.get_flat_sky_projection(flat_sky_pixels_size)
54
55
        # Read map tree (data)
56
57
        self._maptree = map_tree_factory(maptree, roi=roi)
58
59
        # Read detector response_file
60
61
        self._response = hawc_response_factory(response_file)
62
63
        # Use a renormalization of the background as nuisance parameter
64
        # NOTE: it is fixed to 1.0 unless the user explicitly sets it free (experimental)
65
        self._nuisance_parameters = collections.OrderedDict()
66
        self._nuisance_parameters['%s_bkg_renorm' % name] = Parameter('%s_bkg_renorm' % name, 1.0,
67
                                                                      min_value=0.5, max_value=1.5,
68
                                                                      delta=0.01,
69
                                                                      desc="Renormalization for background map",
70
                                                                      free=False,
71
                                                                      is_normalization=False)
72
73
        # Instance parent class
74
75
        super(HAL, self).__init__(name, self._nuisance_parameters)
76
77
        self._likelihood_model = None
78
79
        # These lists will contain the maps for the point sources
80
        self._convolved_point_sources = ConvolvedSourcesContainer()
81
        # and this one for extended sources
82
        self._convolved_ext_sources = ConvolvedSourcesContainer()
83
84
        # All energy/nHit bins are loaded in memory
85
        self._all_planes = list(self._maptree.analysis_bins_labels)
86
87
        # The active planes list always contains the list of *indexes* of the active planes
88
        self._active_planes = None
89
90
        # Set up the transformations from the flat-sky projection to Healpix, as well as the list of active pixels
91
        # (one for each energy/nHit bin). We make a separate transformation because different energy bins might have
92
        # different nsides
93
        self._active_pixels = collections.OrderedDict()
94
        self._flat_sky_to_healpix_transform = collections.OrderedDict()
95
96
        for bin_id in self._maptree:
97
98
            this_maptree = self._maptree[bin_id]
99
            this_nside = this_maptree.nside
100
            this_active_pixels = roi.active_pixels(this_nside)
101
102
            this_flat_sky_to_hpx_transform = FlatSkyToHealpixTransform(self._flat_sky_projection.wcs,
103
                                                                       'icrs',
104
                                                                       this_nside,
105
                                                                       this_active_pixels,
106
                                                                       (self._flat_sky_projection.npix_width,
107
                                                                        self._flat_sky_projection.npix_height),
108
                                                                       order='bilinear')
109
110
            self._active_pixels[bin_id] = this_active_pixels
111
            self._flat_sky_to_healpix_transform[bin_id] = this_flat_sky_to_hpx_transform
112
113
        # This will contain a list of PSF convolutors for extended sources, if there is any in the model
114
115
        self._psf_convolutors = None
116
117
        # Pre-compute the log-factorial factor in the likelihood, so we do not keep to computing it over and over
118
        # again.
119
        self._log_factorials = collections.OrderedDict()
120
121
        # We also apply a bias so that the numerical value of the log-likelihood stays small. This helps when
122
        # fitting with algorithms like MINUIT because the convergence criterium involves the difference between
123
        # two likelihood values, which would be affected by numerical precision errors if the two values are
124
        # too large
125
        self._saturated_model_like_per_maptree = collections.OrderedDict()
126
127
        # The actual computation is in a method so we can recall it on clone (see the get_simulated_dataset method)
128
        self._compute_likelihood_biases()
129
130
        # This will save a clone of self for simulations
131
        self._clone = None
132
133
        # Integration method for the PSF (see psf_integration_method)
134
        self._psf_integration_method = "exact"
135
136
    @property
137
    def psf_integration_method(self):
138
        """
139
        Get or set the method for the integration of the PSF.
140
141
        * "exact" is more accurate but slow, if the position is free to vary it adds a lot of time to the fit. This is
142
        the default, to be used when the position of point sources are fixed. The computation in that case happens only
143
        once so the impact on the run time is negligible.
144
        * "fast" is less accurate (up to an error of few percent in flux) but a lot faster. This should be used when
145
        the position of the point source is free, because in that case the integration of the PSF happens every time
146
        the position changes, so several times during the fit.
147
148
        If you have a fit with a free position, use "fast". When the position is found, you can fix it, switch to
149
        "exact" and redo the fit to obtain the most accurate measurement of the flux. For normal sources the difference
150
        will be small, but for very bright sources it might be up to a few percent (most of the time < 1%). If you are
151
        interested in the localization contour there is no need to rerun with "exact".
152
153
        :param mode: either "exact" or "fast"
154
        :return: None
155
        """
156
157
        return self._psf_integration_method
158
159
    @psf_integration_method.setter
160
    def psf_integration_method(self, mode):
161
162
        assert mode.lower() in ["exact", "fast"], "PSF integration method must be either 'exact' or 'fast'"
163
164
        self._psf_integration_method = mode.lower()
165
166
    def _setup_psf_convolutors(self):
167
168
        central_response_bins = self._response.get_response_dec_bin(self._roi.ra_dec_center[1])
169
170
        self._psf_convolutors = collections.OrderedDict()
171
        for bin_id in central_response_bins:
172
173
            #Only set up PSF convolutors for active bins.
174
            if bin_id in self._active_planes:
175
                self._psf_convolutors[bin_id] = PSFConvolutor(central_response_bins[bin_id].psf,
176
                                                              self._flat_sky_projection)
177
178
179
    def _compute_likelihood_biases(self):
180
181
        for bin_label in self._maptree:
182
183
            data_analysis_bin = self._maptree[bin_label]
184
185
            this_log_factorial = np.sum(logfactorial(data_analysis_bin.observation_map.as_partial()))
186
            self._log_factorials[bin_label] = this_log_factorial
187
188
            # As bias we use the likelihood value for the saturated model
189
            obs = data_analysis_bin.observation_map.as_partial()
190
            bkg = data_analysis_bin.background_map.as_partial()
191
192
            sat_model = np.clip(obs - bkg, 1e-50, None).astype(np.float64)
193
194
            self._saturated_model_like_per_maptree[bin_label] = log_likelihood(obs, bkg, sat_model) - this_log_factorial
195
196
    def get_saturated_model_likelihood(self):
197
        """
198
        Returns the likelihood for the saturated model (i.e. a model exactly equal to observation - background).
199
200
        :return:
201
        """
202
        return sum(self._saturated_model_like_per_maptree.values())
203
204
    def set_active_measurements(self, bin_id_min=None, bin_id_max=None, bin_list=None):
205
        """
206
        Set the active analysis bins to use during the analysis. It can be used in two ways:
207
208
        - Specifying a range: if the response and the maptree allows it, you can specify a minimum id and a maximum id
209
        number. This only works if the analysis bins are numerical, like in the normal fHit analysis. For example:
210
211
            > set_active_measurement(bin_id_min=1, bin_id_max=9(
212
213
        - Specifying a list of bins as strings. This is more powerful, as allows to select any bins, even
214
        non-contiguous bins. For example:
215
216
            > set_active_measurement(bin_list=[list])
217
218
        :param bin_id_min: minimum bin (only works for fHit analysis. For the others, use bin_list)
219
        :param bin_id_max: maximum bin (only works for fHit analysis. For the others, use bin_list)
220
        :param bin_list: a list of analysis bins to use
221
        :return: None
222
        """
223
224
        # Check for legal input
225
        if bin_id_min is not None:
226
227
            assert bin_id_max is not None, "If you provide a minimum bin, you also need to provide a maximum bin"
228
229
            # Make sure they are integers
230
            bin_id_min = int(bin_id_min)
231
            bin_id_max = int(bin_id_max)
232
233
            self._active_planes = []
234
            for this_bin in range(bin_id_min, bin_id_max + 1):
235
                this_bin = str(this_bin)
236
                if this_bin not in self._all_planes:
237
238
                    raise ValueError("Bin %s it not contained in this response" % this_bin)
239
240
                self._active_planes.append(this_bin)
241
242
        else:
243
244
            assert bin_id_max is None, "If you provide a maximum bin, you also need to provide a minimum bin"
245
246
            assert bin_list is not None
247
248
            self._active_planes = []
249
250
            for this_bin in bin_list:
251
252
                if not this_bin in self._all_planes:
253
254
                    raise ValueError("Bin %s it not contained in this response" % this_bin)
255
256
                self._active_planes.append(this_bin)
257
258
        if self._likelihood_model:
259
260
            self.set_model( self._likelihood_model )
0 ignored issues
show
Coding Style introduced by
No space allowed after bracket
Loading history...
Coding Style introduced by
No space allowed before bracket
Loading history...
261
262
    def display(self, verbose=False):
263
        """
264
        Prints summary of the current object content.
265
        """
266
267
        print("Region of Interest: ")
268
        print("-------------------\n")
269
        self._roi.display()
270
271
        print("")
272
        print("Flat sky projection: ")
273
        print("--------------------\n")
274
275
        print("Width x height: %s x %s px" % (self._flat_sky_projection.npix_width,
276
                                              self._flat_sky_projection.npix_height))
277
        print("Pixel sizes: %s deg" % self._flat_sky_projection.pixel_size)
278
279
        print("")
280
        print("Response: ")
281
        print("---------\n")
282
283
        self._response.display(verbose)
284
285
        print("")
286
        print("Map Tree: ")
287
        print("----------\n")
288
289
        self._maptree.display()
290
291
        print("")
292
        print("Active energy/nHit planes ({}):".format(len(self._active_planes)))
293
        print("-------------------------------\n")
294
        print(self._active_planes)
295
296
    def set_model(self, likelihood_model_instance):
297
        """
298
        Set the model to be used in the joint minimization. Must be a LikelihoodModel instance.
299
        """
300
301
        self._likelihood_model = likelihood_model_instance
302
303
        # Reset
304
        self._convolved_point_sources.reset()
305
        self._convolved_ext_sources.reset()
306
307
        # For each point source in the model, build the convolution class
308
309
        for source in self._likelihood_model.point_sources.values():
310
311
            this_convolved_point_source = ConvolvedPointSource(source, self._response, self._flat_sky_projection)
312
313
            self._convolved_point_sources.append(this_convolved_point_source)
314
315
        # Samewise for extended sources
316
317
        ext_sources = self._likelihood_model.extended_sources.values()
318
319
        # NOTE: ext_sources evaluate to False if empty
320
        if ext_sources:
321
322
            # We will need to convolve
323
324
            self._setup_psf_convolutors()
325
326
            for source in ext_sources:
327
328
                if source.spatial_shape.n_dim == 2:
329
330
                    this_convolved_ext_source = ConvolvedExtendedSource2D(source,
331
                                                                          self._response,
332
                                                                          self._flat_sky_projection)
333
334
                else:
335
336
                    this_convolved_ext_source = ConvolvedExtendedSource3D(source,
337
                                                                          self._response,
338
                                                                          self._flat_sky_projection)
339
340
                self._convolved_ext_sources.append(this_convolved_ext_source)
341
342
    def display_spectrum(self):
0 ignored issues
show
Comprehensibility introduced by
This function exceeds the maximum number of variables (24/15).
Loading history...
343
        """
344
        Make a plot of the current spectrum and its residuals (integrated over space)
345
346
        :return: a matplotlib.Figure
347
        """
348
349
        n_point_sources = self._likelihood_model.get_number_of_point_sources()
350
        n_ext_sources = self._likelihood_model.get_number_of_extended_sources()
351
352
        total_counts = np.zeros(len(self._active_planes), dtype=float)
353
        total_model = np.zeros_like(total_counts)
354
        model_only = np.zeros_like(total_counts)
355
        net_counts = np.zeros_like(total_counts)
356
        yerr_low = np.zeros_like(total_counts)
357
        yerr_high = np.zeros_like(total_counts)
358
359
        for i, energy_id in enumerate(self._active_planes):
360
361
            data_analysis_bin = self._maptree[energy_id]
362
363
            this_model_map_hpx = self._get_expectation(data_analysis_bin, energy_id, n_point_sources, n_ext_sources)
364
365
            this_model_tot = np.sum(this_model_map_hpx)
366
367
            this_data_tot = np.sum(data_analysis_bin.observation_map.as_partial())
368
            this_bkg_tot = np.sum(data_analysis_bin.background_map.as_partial())
369
370
            total_counts[i] = this_data_tot
371
            net_counts[i] = this_data_tot - this_bkg_tot
372
            model_only[i] = this_model_tot
373
374
            this_wh_model = this_model_tot + this_bkg_tot
375
            total_model[i] = this_wh_model
376
377
            if this_data_tot >= 50.0:
378
379
                # Gaussian limit
380
                # Under the null hypothesis the data are distributed as a Gaussian with mu = model
381
                # and sigma = sqrt(model)
382
                # NOTE: since we neglect the background uncertainty, the background is part of the
383
                # model
384
                yerr_low[i] = np.sqrt(this_data_tot)
385
                yerr_high[i] = np.sqrt(this_data_tot)
386
387
            else:
388
389
                # Low-counts
390
                # Under the null hypothesis the data are distributed as a Poisson distribution with
391
                # mean = model, plot the 68% confidence interval (quantile=[0.16,1-0.16]).
392
                # NOTE: since we neglect the background uncertainty, the background is part of the
393
                # model
394
                quantile = 0.16
395
                mean = this_wh_model
396
                y_low = poisson.isf(1-quantile, mu=mean)
397
                y_high = poisson.isf(quantile, mu=mean)
398
                yerr_low[i] = mean-y_low
399
                yerr_high[i] = y_high-mean
400
401
        residuals = (total_counts - total_model) / np.sqrt(total_model)
402
        residuals_err = [yerr_high / np.sqrt(total_model),
403
                         yerr_low / np.sqrt(total_model)]
404
405
        yerr = [yerr_high, yerr_low]
406
407
        return self._plot_spectrum(net_counts, yerr, model_only, residuals, residuals_err)
408
409
    def _plot_spectrum(self, net_counts, yerr, model_only, residuals, residuals_err):
0 ignored issues
show
best-practice introduced by
Too many arguments (6/5)
Loading history...
410
411
        fig, subs = plt.subplots(2, 1, gridspec_kw={'height_ratios': [2, 1], 'hspace': 0})
412
413
        subs[0].errorbar(self._active_planes, net_counts, yerr=yerr,
414
                         capsize=0,
415
                         color='black', label='Net counts', fmt='.')
416
417
        subs[0].plot(self._active_planes, model_only, label='Convolved model')
418
419
        subs[0].legend(bbox_to_anchor=(1.0, 1.0), loc="upper right",
420
                       numpoints=1)
421
422
        # Residuals
423
        subs[1].axhline(0, linestyle='--')
424
425
        subs[1].errorbar(
426
            self._active_planes, residuals,
427
            yerr=residuals_err,
428
            capsize=0, fmt='.'
429
        )
430
431
        y_limits = [min(net_counts[net_counts > 0]) / 2., max(net_counts) * 2.]
432
433
        subs[0].set_yscale("log", nonposy='clip')
434
        subs[0].set_ylabel("Counts per bin")
435
        subs[0].set_xticks([])
436
437
        subs[1].set_xlabel("Analysis bin")
438
        subs[1].set_ylabel(r"$\frac{{cts - mod - bkg}}{\sqrt{mod + bkg}}$")
439
        subs[1].set_xticks(self._active_planes)
440
        subs[1].set_xticklabels(self._active_planes)
441
442
        subs[0].set_ylim(y_limits)
443
444
        return fig
445
446
    def get_log_like(self):
447
        """
448
        Return the value of the log-likelihood with the current values for the
449
        parameters
450
        """
451
452
        n_point_sources = self._likelihood_model.get_number_of_point_sources()
453
        n_ext_sources = self._likelihood_model.get_number_of_extended_sources()
454
455
        # Make sure that no source has been added since we filled the cache
456
        assert n_point_sources == self._convolved_point_sources.n_sources_in_cache and \
457
               n_ext_sources == self._convolved_ext_sources.n_sources_in_cache, \
458
            "The number of sources has changed. Please re-assign the model to the plugin."
459
460
        # This will hold the total log-likelihood
461
        total_log_like = 0
462
463
        for bin_id in self._active_planes:
464
465
            data_analysis_bin = self._maptree[bin_id]
466
467
            this_model_map_hpx = self._get_expectation(data_analysis_bin, bin_id, n_point_sources, n_ext_sources)
468
469
            # Now compare with observation
470
            bkg_renorm = self._nuisance_parameters.values()[0].value
471
472
            obs = data_analysis_bin.observation_map.as_partial()  # type: np.array
473
            bkg = data_analysis_bin.background_map.as_partial() * bkg_renorm  # type: np.array
474
475
            this_pseudo_log_like = log_likelihood(obs,
476
                                                  bkg,
477
                                                  this_model_map_hpx)
478
479
            total_log_like += this_pseudo_log_like - self._log_factorials[bin_id] \
480
                              - self._saturated_model_like_per_maptree[bin_id]
481
482
        return total_log_like
483
484
    def write(self, response_file_name, map_tree_file_name):
485
        """
486
        Write this dataset to disk in HDF format.
487
488
        :param response_file_name: filename for the response
489
        :param map_tree_file_name: filename for the map tree
490
        :return: None
491
        """
492
493
        self._maptree.write(map_tree_file_name)
494
        self._response.write(response_file_name)
495
496
    def get_simulated_dataset(self, name):
497
        """
498
        Return a simulation of this dataset using the current model with current parameters.
499
500
        :param name: new name for the new plugin instance
501
        :return: a HAL instance
502
        """
503
504
505
        # First get expectation under the current model and store them, if we didn't do it yet
506
507
        if self._clone is None:
508
509
            n_point_sources = self._likelihood_model.get_number_of_point_sources()
510
            n_ext_sources = self._likelihood_model.get_number_of_extended_sources()
511
512
            expectations = collections.OrderedDict()
513
514
            for bin_id in self._maptree:
515
516
                data_analysis_bin = self._maptree[bin_id]
517
                if bin_id not in self._active_planes:
518
519
                    expectations[bin_id] = None
520
521
                else:
522
523
                    expectations[bin_id] = self._get_expectation(data_analysis_bin, bin_id,
524
                        n_point_sources, n_ext_sources) + \
0 ignored issues
show
Coding Style introduced by
Wrong continued indentation (add 41 spaces).
Loading history...
525
                        data_analysis_bin.background_map.as_partial()
526
527
            if parallel_client.is_parallel_computation_active():
528
529
                # Do not clone, as the parallel environment already makes clones
530
531
                clone = self
532
533
            else:
534
535
                clone = copy.deepcopy(self)
536
537
            self._clone = (clone, expectations)
538
539
        # Substitute the observation and background for each data analysis bin
540
        for bin_id in self._clone[0]._maptree:
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _maptree was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
541
542
            data_analysis_bin = self._clone[0]._maptree[bin_id]
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _maptree was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
543
544
            if bin_id not in self._active_planes:
545
546
                continue
547
548
            else:
549
550
                # Active plane. Generate new data
551
                expectation = self._clone[1][bin_id]
552
                new_data = np.random.poisson(expectation, size=(1, expectation.shape[0])).flatten()
553
554
                # Substitute data
555
                data_analysis_bin.observation_map.set_new_values(new_data)
556
557
        # Now change name and return
558
        self._clone[0]._name = name
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _name was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
559
        # Adjust the name of the nuisance parameter
560
        old_name = self._clone[0]._nuisance_parameters.keys()[0]
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _nuisance_parameters was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
561
        new_name = old_name.replace(self.name, name)
562
        self._clone[0]._nuisance_parameters[new_name] = self._clone[0]._nuisance_parameters.pop(old_name)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _nuisance_parameters was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
563
564
        # Recompute biases
565
        self._clone[0]._compute_likelihood_biases()
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _compute_likelihood_biases was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
566
567
        return self._clone[0]
568
569
    def _get_expectation(self, data_analysis_bin, energy_bin_id, n_point_sources, n_ext_sources):
570
571
        # Compute the expectation from the model
572
573
        this_model_map = None
574
575
        for pts_id in range(n_point_sources):
576
577
            this_conv_src = self._convolved_point_sources[pts_id]
578
579
            expectation_per_transit = this_conv_src.get_source_map(energy_bin_id,
580
                                                                   tag=None,
581
                                                                   psf_integration_method=self._psf_integration_method)
582
583
            expectation_from_this_source = expectation_per_transit * data_analysis_bin.n_transits
584
585
            if this_model_map is None:
586
587
                # First addition
588
589
                this_model_map = expectation_from_this_source
590
591
            else:
592
593
                this_model_map += expectation_from_this_source
594
595
        # Now process extended sources
596
        if n_ext_sources > 0:
597
598
            this_ext_model_map = None
599
600
            for ext_id in range(n_ext_sources):
601
602
                this_conv_src = self._convolved_ext_sources[ext_id]
603
604
                expectation_per_transit = this_conv_src.get_source_map(energy_bin_id)
605
606
                if this_ext_model_map is None:
607
608
                    # First addition
609
610
                    this_ext_model_map = expectation_per_transit
611
612
                else:
613
614
                    this_ext_model_map += expectation_per_transit
615
616
            # Now convolve with the PSF
617
            if this_model_map is None:
618
                
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
619
                # Only extended sources
620
            
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
621
                this_model_map = (self._psf_convolutors[energy_bin_id].extended_source_image(this_ext_model_map) *
622
                                  data_analysis_bin.n_transits)
623
            
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
624
            else:
625
626
                this_model_map += (self._psf_convolutors[energy_bin_id].extended_source_image(this_ext_model_map) *
627
                                   data_analysis_bin.n_transits)
628
629
630
        # Now transform from the flat sky projection to HEALPiX
631
632
        if this_model_map is not None:
633
634
            # First divide for the pixel area because we need to interpolate brightness
635
            this_model_map = this_model_map / self._flat_sky_projection.project_plane_pixel_area
636
637
            this_model_map_hpx = self._flat_sky_to_healpix_transform[energy_bin_id](this_model_map, fill_value=0.0)
638
639
            # Now multiply by the pixel area of the new map to go back to flux
640
            this_model_map_hpx *= hp.nside2pixarea(data_analysis_bin.nside, degrees=True)
641
642
        else:
643
644
            # No sources
645
646
            this_model_map_hpx = 0.0
647
648
        return this_model_map_hpx
649
650
    @staticmethod
651
    def _represent_healpix_map(fig, hpx_map, longitude, latitude, xsize, resolution, smoothing_kernel_sigma):
0 ignored issues
show
best-practice introduced by
Too many arguments (7/5)
Loading history...
652
653
        proj = get_gnomonic_projection(fig, hpx_map,
654
                                       rot=(longitude, latitude, 0.0),
655
                                       xsize=xsize,
656
                                       reso=resolution)
657
658
        if smoothing_kernel_sigma is not None:
659
660
            # Get the sigma in pixels
661
            sigma = smoothing_kernel_sigma * 60 / resolution
662
663
            proj = convolve(list(proj),
664
                            Gaussian2DKernel(sigma),
665
                            nan_treatment='fill',
666
                            preserve_nan=True)
667
668
        return proj
669
670
    def display_fit(self, smoothing_kernel_sigma=0.1, display_colorbar=False):
0 ignored issues
show
Comprehensibility introduced by
This function exceeds the maximum number of variables (32/15).
Loading history...
671
        """
672
        Make a figure containing 4 maps for each active analysis bins with respectively model, data,
673
        background and residuals. The model, data and residual maps are smoothed, the background
674
        map is not.
675
676
        :param smoothing_kernel_sigma: sigma for the Gaussian smoothing kernel, for all but
677
        background maps
678
        :param display_colorbar: whether or not to display the colorbar in the residuals
679
        :return: a matplotlib.Figure
680
        """
681
682
        n_point_sources = self._likelihood_model.get_number_of_point_sources()
683
        n_ext_sources = self._likelihood_model.get_number_of_extended_sources()
684
685
        # This is the resolution (i.e., the size of one pixel) of the image
686
        resolution = 3.0  # arcmin
687
688
        # The image is going to cover the diameter plus 20% padding
689
        xsize = self._get_optimal_xsize(resolution)
690
691
        n_active_planes = len(self._active_planes)
692
        n_columns = 4
693
694
        fig, subs = plt.subplots(n_active_planes, n_columns,
695
                                 figsize=(2.7 * n_columns, n_active_planes * 2))
696
697
        with progress_bar(len(self._active_planes), title='Smoothing maps') as prog_bar:
698
699
            images = ['None'] * n_columns
700
701
            for i, plane_id in enumerate(self._active_planes):
702
703
                data_analysis_bin = self._maptree[plane_id]
704
705
                # Get the center of the projection for this plane
706
                this_ra, this_dec = self._roi.ra_dec_center
707
708
                # Make a full healpix map for a second
709
                whole_map = self._get_model_map(plane_id, n_point_sources, n_ext_sources).as_dense()
710
711
                # Healpix uses longitude between -180 and 180, while R.A. is between 0 and 360. We need to fix that:
712
                longitude = ra_to_longitude(this_ra)
713
714
                # Declination is already between -90 and 90
715
                latitude = this_dec
716
717
                # Background and excess maps
718
                bkg_subtracted, _, background_map = self._get_excess(data_analysis_bin, all_maps=True)
719
720
                # Make all the projections: model, excess, background, residuals
721
                proj_model = self._represent_healpix_map(fig, whole_map,
722
                                                         longitude, latitude,
723
                                                         xsize, resolution, smoothing_kernel_sigma)
724
                # Here we removed the background otherwise nothing is visible
725
                # Get background (which is in a way "part of the model" since the uncertainties are neglected)
726
                proj_data = self._represent_healpix_map(fig, bkg_subtracted,
727
                                                        longitude, latitude,
728
                                                        xsize, resolution, smoothing_kernel_sigma)
729
                # No smoothing for this one (because a goal is to check it is smooth).
730
                proj_bkg = self._represent_healpix_map(fig, background_map,
731
                                                       longitude, latitude,
732
                                                       xsize, resolution, None)
733
                proj_residuals = proj_data - proj_model
734
735
                # Common color scale range for model and excess maps
736
                vmin = min(np.nanmin(proj_model), np.nanmin(proj_data))
737
                vmax = max(np.nanmax(proj_model), np.nanmax(proj_data))
738
739
                # Plot model
740
                images[0] = subs[i][0].imshow(proj_model, origin='lower', vmin=vmin, vmax=vmax)
741
                subs[i][0].set_title('model, bin {}'.format(data_analysis_bin.name))
742
743
                # Plot data map
744
                images[1] = subs[i][1].imshow(proj_data, origin='lower', vmin=vmin, vmax=vmax)
745
                subs[i][1].set_title('excess, bin {}'.format(data_analysis_bin.name))
746
747
                # Plot background map.
748
                images[2] = subs[i][2].imshow(proj_bkg, origin='lower')
749
                subs[i][2].set_title('background, bin {}'.format(data_analysis_bin.name))
750
751
                # Now residuals
752
                images[3] = subs[i][3].imshow(proj_residuals, origin='lower')
753
                subs[i][3].set_title('residuals, bin {}'.format(data_analysis_bin.name))
754
755
                # Remove numbers from axis
756
                for j in range(n_columns):
757
                    subs[i][j].axis('off')
758
759
                if display_colorbar:
760
                    for j, image in enumerate(images):
761
                        plt.colorbar(image, ax=subs[i][j])
762
763
                prog_bar.increase()
764
765
        fig.set_tight_layout(True)
766
767
        return fig
768
769
    def _get_optimal_xsize(self, resolution):
770
771
        return 2.2 * self._roi.data_radius.to("deg").value / (resolution / 60.0)
772
773
    def display_stacked_image(self, smoothing_kernel_sigma=0.5):
0 ignored issues
show
Comprehensibility introduced by
This function exceeds the maximum number of variables (21/15).
Loading history...
774
        """
775
        Display a map with all active analysis bins stacked together.
776
777
        :param smoothing_kernel_sigma: sigma for the Gaussian smoothing kernel to apply
778
        :return: a matplotlib.Figure instance
779
        """
780
781
        # This is the resolution (i.e., the size of one pixel) of the image in arcmin
782
        resolution = 3.0
783
784
        # The image is going to cover the diameter plus 20% padding
785
        xsize = self._get_optimal_xsize(resolution)
786
787
        active_planes_bins = [self._maptree[x] for x in self._active_planes]
788
789
        # Get the center of the projection for this plane
790
        this_ra, this_dec = self._roi.ra_dec_center
791
792
        # Healpix uses longitude between -180 and 180, while R.A. is between 0 and 360. We need to fix that:
793
        longitude = ra_to_longitude(this_ra)
794
795
        # Declination is already between -90 and 90
796
        latitude = this_dec
797
798
        total = None
799
800
        for i, data_analysis_bin in enumerate(active_planes_bins):
801
802
            # Plot data
803
            background_map = data_analysis_bin.background_map.as_dense()
804
            this_data = data_analysis_bin.observation_map.as_dense() - background_map
805
            idx = np.isnan(this_data)
806
            # this_data[idx] = hp.UNSEEN
807
808
            if i == 0:
809
810
                total = this_data
811
812
            else:
813
814
                # Sum only when there is no UNSEEN, so that the UNSEEN pixels will stay UNSEEN
815
                total[~idx] += this_data[~idx]
816
817
        delta_coord = (self._roi.data_radius.to("deg").value * 2.0) / 15.0
818
819
        fig, sub = plt.subplots(1, 1)
820
821
        proj = self._represent_healpix_map(fig, total, longitude, latitude, xsize, resolution, smoothing_kernel_sigma)
822
823
        cax = sub.imshow(proj, origin='lower')
824
        fig.colorbar(cax)
825
        sub.axis('off')
826
827
        hp.graticule(delta_coord, delta_coord)
828
829
        return fig
830
831
    def inner_fit(self):
832
        """
833
        This is used for the profile likelihood. Keeping fixed all parameters in the
834
        LikelihoodModel, this method minimize the logLike over the remaining nuisance
835
        parameters, i.e., the parameters belonging only to the model for this
836
        particular detector. If there are no nuisance parameters, simply return the
837
        logLike value.
838
        """
839
840
        return self.get_log_like()
841
842
    def get_number_of_data_points(self):
843
        """
844
        Return the number of active bins across all active analysis bins
845
846
        :return: number of active bins
847
        """
848
849
        n_points = 0
850
851
        for bin_id in self._maptree:
852
            n_points += self._maptree[bin_id].observation_map.as_partial().shape[0]
853
854
        return n_points
855
856
    def _get_model_map(self, plane_id, n_pt_src, n_ext_src):
857
        """
858
        This function returns a model map for a particular bin
859
        """
860
861
        if plane_id not in self._active_planes:
862
            raise ValueError("{0} not a plane in the current model".format(plane_id))
863
864
        model_map = SparseHealpix(self._get_expectation(self._maptree[plane_id], plane_id, n_pt_src, n_ext_src),
865
                                  self._active_pixels[plane_id],
866
                                  self._maptree[plane_id].observation_map.nside)
867
868
        return model_map
869
870
    def _get_excess(self, data_analysis_bin, all_maps=True):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
871
        """
872
        This function returns the excess counts for a particular bin
873
        if all_maps=True, also returns the data and background maps
874
        """
875
        data_map = data_analysis_bin.observation_map.as_dense()
876
        bkg_map = data_analysis_bin.background_map.as_dense()
877
        excess = data_map - bkg_map
878
879
        if all_maps:
880
            return excess, data_map, bkg_map
881
        return excess
882
883
    def _write_a_map(self, file_name, which, fluctuate=False, return_map=False):
0 ignored issues
show
Comprehensibility introduced by
This function exceeds the maximum number of variables (16/15).
Loading history...
Unused Code introduced by
Either all return statements in a function should return an expression, or none of them should.
Loading history...
884
        """
885
        This writes either a model map or a residual map, depending on which one is preferred
886
        """
887
        which = which.lower()
888
        assert which in ['model', 'residual']
889
890
        n_pt = self._likelihood_model.get_number_of_point_sources()
891
        n_ext = self._likelihood_model.get_number_of_extended_sources()
892
893
        map_analysis_bins = collections.OrderedDict()
894
895
        if fluctuate:
896
            poisson_set = self.get_simulated_dataset("model map")
897
898
        for plane_id in self._active_planes:
899
900
            data_analysis_bin = self._maptree[plane_id]
901
902
            bkg = data_analysis_bin.background_map
903
            obs = data_analysis_bin.observation_map
904
905
            if fluctuate:
906
                model_excess = poisson_set._maptree[plane_id].observation_map \
0 ignored issues
show
introduced by
The variable poisson_set does not seem to be defined in case fluctuate on line 895 is False. Are you sure this can never be the case?
Loading history...
Coding Style Best Practice introduced by
It seems like _maptree was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
907
                               - poisson_set._maptree[plane_id].background_map
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _maptree was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
908
            else:
909
                model_excess = self._get_model_map(plane_id, n_pt, n_ext)
910
911
            if which == 'residual':
912
                bkg += model_excess
913
914
            if which == 'model':
915
                obs = model_excess + bkg
916
917
            this_bin = DataAnalysisBin(plane_id,
918
                                       observation_hpx_map=obs,
919
                                       background_hpx_map=bkg,
920
                                       active_pixels_ids=self._active_pixels[plane_id],
921
                                       n_transits=data_analysis_bin.n_transits,
922
                                       scheme='RING')
923
924
            map_analysis_bins[plane_id] = this_bin
925
926
        # save the file
927
        new_map_tree = MapTree(map_analysis_bins, self._roi)
928
        new_map_tree.write(file_name)
929
930
        if return_map:
931
            return new_map_tree
932
933
    def write_model_map(self, file_name, poisson_fluctuate=False, test_return_map=False):
0 ignored issues
show
Unused Code introduced by
Either all return statements in a function should return an expression, or none of them should.
Loading history...
934
        """
935
        This function writes the model map to a file. (it is currently not implemented)
936
        The interface is based off of HAWCLike for consistency
937
        """
938
        if test_return_map:
939
            print("You should only return a model map here for testing purposes!")
940
            return self._write_a_map(file_name, 'model', poisson_fluctuate, test_return_map)
941
        else:
942
            self._write_a_map(file_name, 'model', poisson_fluctuate, test_return_map)
943
944
    def write_residual_map(self, file_name, test_return_map=False):
0 ignored issues
show
Unused Code introduced by
Either all return statements in a function should return an expression, or none of them should.
Loading history...
945
        """
946
        This function writes the residual map to a file. (it is currently not implemented)
947
        The interface is based off of HAWCLike for consistency
948
        """
949
        if test_return_map:
950
            print("You should only return a residual map here for testing purposes!")
951
            return self._write_a_map(file_name, 'residual', False, test_return_map)
952
        else:
953
            self._write_a_map(file_name, 'residual', False, test_return_map)
954