obs2background.py

Kelley-Hoskins Nathan, 10/02/2015 09:24 PM

Download (14.4 KB)

 
1
#!/usr/bin/env python
2
# author: Nathan Kelley-Hoskins
3
# date  : Oct, 2015
4
#
5
# Run this script with no arguments to see the usage, 
6
# or scroll to the bottom of this script.
7
#
8
import sys, gammalib, math, numpy
9
from scipy import ndimage
10
from astropy.io import fits
11

    
12
def denoise_tv_chambolle_2d(im, weight=50, eps=2.e-4, n_iter_max=200):
13
    """Perform total-variation denoising on 2D images.
14
    Parameters
15
    ----------
16
    im : ndarray
17
        Input data to be denoised.
18
    weight : float, optional
19
        Denoising weight. The greater `weight`, the more denoising (at
20
        the expense of fidelity to `input`)
21
    eps : float, optional
22
        Relative difference of the value of the cost function that determines
23
        the stop criterion. The algorithm stops when:
24
            (E_(n-1) - E_n) < eps * E_0
25
    n_iter_max : int, optional
26
        Maximal number of iterations used for the optimization.
27
    Returns
28
    -------
29
    out : ndarray
30
        Denoised array of floats.
31
    Notes
32
    -----
33

34
    ***Function taken from the python module scikit-image***
35
    ***https://github.com/scikit-image/scikit-image/blob/master/skimage/restoration/_denoise.py***
36

37
    The principle of total variation denoising is explained in
38
    http://en.wikipedia.org/wiki/Total_variation_denoising.
39
    This code is an implementation of the algorithm of Rudin, Fatemi and Osher
40
    that was proposed by Chambolle in [1]_.
41
    References
42
    ----------
43
    .. [1] A. Chambolle, An algorithm for total variation minimization and
44
           applications, Journal of Mathematical Imaging and Vision,
45
           Springer, 2004, 20, 89-97.
46
    """
47

    
48
    px = numpy.zeros_like(im)
49
    py = numpy.zeros_like(im)
50
    gx = numpy.zeros_like(im)
51
    gy = numpy.zeros_like(im)
52
    d = numpy.zeros_like(im)
53
    i = 0
54
    while i < n_iter_max:
55
        d = -px - py
56
        d[1:]    += px[:-1]
57
        d[:, 1:] += py[:, :-1]
58

    
59
        out = im + d
60
        E   = (d ** 2).sum()
61
        gx[:-1]    = numpy.diff(out, axis=0)  # [], gradient/variation in x direction
62
        gy[:, :-1] = numpy.diff(out, axis=1)  # [], gradient/variation in y direction 
63
        norm  = numpy.sqrt(gx ** 2 + gy ** 2) # total variation here
64
        E    += weight * norm.sum()           # adjust by weight between sameness and variation
65
        norm *= 0.5 / weight
66
        norm += 1
67
        px   -= 0.25 * gx
68
        px   /= norm
69
        py   -= 0.25 * gy
70
        py   /= norm
71
        E    /= float(im.size)
72
        if i == 0:
73
            E_init     = E
74
            E_previous = E
75
        else:
76
            if numpy.abs(E_previous - E) < eps * E_init:
77
                break
78
            else:
79
                E_previous = E
80
        i += 1
81
    return out
82

    
83

    
84
def background2fits( data, fname ) :
85
  """Function just for taking final background data, and writing it to a fits file.
86
  Does no binning or smearing.
87
  
88
  **Args:**
89
    data  : dictionary containing info about background, see write_background_to_fits for more info.
90
    fname : output fits filename to write background to
91
  """
92
  # figure out basics of the fits table
93
  ndetx   = len( data['DETX_LO'] )
94
  ndety   = len( data['DETY_LO'] )
95
  nenez   = len( data['BGD'    ] )
96
  formx   = '%dE' % ndetx
97
  formz   = '%dE' % nenez
98
  formxyz = '%dE' % ( ndetx * ndety * nenez )
99
  bgddim  = '(%d,%d,%d)' % ( ndetx, ndety, nenez ) 
100
  
101
  # setup table columns
102
  #print
103
  cols = []
104
  for key in [ 'DETX_LO', 'DETX_HI', 'DETY_LO', 'DETY_HI' ] :
105
    #print 'adding col %s with %d elements: ' % ( key, len(data[key]) ), [ '%.2f'%d for d in data[key] ]
106
    cols.append( fits.Column( name=key , format=formx  , unit='deg'       , array=[ data[key] ]             ) )
107
  for key in [ 'ENERG_LO', 'ENERG_HI' ] :
108
    #print 'adding col %s' % key
109
    encol = [ en.TeV() for en in data[key] ] # convert from list of GEnergy's to list of TeV floats
110
    cols.append( fits.Column( name=key , format=formz  , unit='TeV'       , array=[ encol     ]             ) )
111
  for key in [ 'BGD' ] :
112
    #print 'adding col %s' % key
113
    cols.append( fits.Column( name=key , format=formxyz, unit='1/s/MeV/sr', array=[ data[key] ], dim=bgddim ) )
114

    
115
  # setup fits table
116
  tablehdu      = fits.BinTableHDU.from_columns( cols )
117
  
118
  # name the table and make sure the dimensionality of the BGD column is set correctly
119
  tablehdu.name = 'BACKGROUND'
120
  tablehdu.header.set( 'TDIM7', bgddim )
121
  
122
  # write table to file
123
  print 'writing fits table to %s' % fname
124
  tablehdu.writeto( fname, clobber=True)
125

    
126
def write_background_to_fits( obs, fname, binwidth=0.4, nbufferbin=5, nenergybins=5, energyprange=[0.02,0.99], zeroval=1e-5, smoothmode='gauss', smooth=0.4 ) :
127
  """Create and write a background from a GObservations object to a fits file.
128
  
129
  **Arguments:**
130
    obs   : GObservations object
131
    fname : str, filename of output fits file
132
  
133
  **Options:**
134
    background binning options:
135
      binwidth     : float, width in degrees of one side of a bin
136
      nbufferbin   : int, number of extra (empty) buffer bins to add to the 
137
                     edge of the camera before smoothing.  If 0, will only
138
                     create bins out to event with largest camera offset.
139
      nenergybins  : int, number of equal-statistics energy bins to create
140
      energyprange : list of 2 floats, [min,max]. Only use the events whose 
141
                     energy is between the min and max percentiles. [0.0,1.0] 
142
                     for all events.
143
      zeroval      : float, after binning events, replace all 0-event bins 
144
                     with this value
145
      smoothmode   : str, type of smoothing to apply to background
146
                     'gauss' : smooth each bin with a 'smooth'-degree-wide gaussian
147
                     'totalvariation' : apply total-variation denoising, with
148
                      weight 'smooth'.  See denoise_tv_chambolle_2d() for
149
                      more information.
150
                     'none' for no smoothing
151
                    
152
      smooth       : float, if smoothmode is 'gauss', then smooth determines
153
                     the gaussian width [deg] of the smoothing.  if smoothmode is
154
                     'totalvariation', smooth is the weight given to the function
155
                     denoise_tv_chambolle_2d(..., weight=smooth)
156
    
157
  **Returns:**
158
    dictionary with info about background produced, keys are:
159
      ['enbins'    ] : list of pairs of energies, one pair for each energy bin
160
      ['detx'      ] : list of detx pairs of angles for camera bin edges
161
      ['dety'      ] : list of detx pairs of angles for camera bin edges
162
      ['bgd'       ] : background counts 3D array, [ienergy][idetx][idety]
163
      ['bgd_smooth'] : same as 'bgd', except smoothed
164
      ['bgd_norm'  ] : same as 'bgd_smooth', except normalized for exposure
165
    
166
"""
167
  
168
  print
169
  print 'Using %s smoothing!' % smoothmode
170
  print
171
  # init some vars
172
  quart_min = min( energyprange )
173
  quart_max = max( energyprange )
174

    
175
  # figure out min/max energy range, max offset
176
  nev = 0
177
  e_min, e_max = 0.0, 0.0
178
  maxoffset = 0.0
179
  for run in obs :
180
    for ev in run.events() :
181
      en = ev.energy()
182
      dx = ev.dir().detx() * gammalib.rad2deg
183
      dy = ev.dir().dety() * gammalib.rad2deg
184
      offset = math.sqrt( dx*dx + dy*dy )
185
      if offset > maxoffset :
186
        maxoffset = offset
187
      if nev == 0 :
188
        e_min = en
189
        e_max = en
190
      if en > e_max : e_max = en
191
      if en < e_min : e_min = en
192
      nev += 1
193
  binr    = maxoffset + ( nbufferbin * binwidth )
194

    
195
  # push e_max up by a tiny amount,
196
  # to include the highest event in the binning
197
  e_max = e_max + gammalib.GEnergy(1,'GeV')
198
  print 'total: %d events from %.3f to %.1f TeV' % ( nev, e_min.TeV(), e_max.TeV() )
199

    
200
  # setup fine-energy binning
201
  nfinebins      = 1000
202
  uselogbinning  = True
203
  enfinespec     = gammalib.GEbounds( nfinebins, e_min, e_max, uselogbinning )
204
  enfinebins     = numpy.zeros( nfinebins )
205
  enfinebinwidth = (e_max.log10TeV() - e_min.log10TeV() ) / nfinebins
206

    
207
  # loop over events, adding to fine energy bins
208
  for run in obs :
209
    for ev in run.events() :
210
      enfinebins[ enfinespec.index( ev.energy() ) ] += 1
211

    
212
  # only use events that are within 
213
  # quart_min and quart_max of the distribution
214

    
215
  # search for energy that contains the given fraction of events at
216
  # that energy or lower
217
  def frac2en( fraclist ) :
218
    """search binned histogram enfinespec (defined a few lines up) for the 
219
   energies that have the given fraction of events at or below that energy.
220
    
221
    **Example:**
222
      frac2en( [0.4 ] ) # find what energy has 40% of events below that energy
223
      frac2en( [0.88] ) # find what energy has 88% of events below that energy
224
    
225
    **Args:**
226
      fraclist : float or list of floats, each float from 0.0 to 1.0
227
    
228
    **Returns:**
229
      list of gammalib.GEnergy objects, each element corrosponding 
230
        to each fraction in 'fraclist'
231
  """
232
    # force input to be a list
233
    if type(fraclist) is not type( [] ) :
234
      fraclist = [ fraclist ]
235
    
236
    # init variables
237
    enlist = [ 0 for i in range(len(fraclist)) ]
238
    last_nfrac = 0.0
239

    
240
    # search histogram bins for given fraction
241
    for i in range(enfinespec.size()) :
242
      nfrac = numpy.sum(enfinebins[:i]) / float(nev)
243
      
244
      # loop over input fractions
245
      for i_frac, frac in enumerate( fraclist ) :
246
        
247
        # when found, 
248
        if last_nfrac < frac and nfrac > frac :
249
          enlist[i_frac] = enfinespec.elogmean(i)
250
      last_nfrac = nfrac
251
    
252
    return enlist
253

    
254
  # calc median/quartile energies
255
  quart_min_en, quart_median_en, quart_max_en = frac2en( [ quart_min, 0.5, quart_max ] )
256

    
257
  # calculate quartile boundaries for each energy bin
258
  quartbins = []
259
  quart_width = ( quart_max - quart_min ) / nenergybins
260
  for i in range(nenergybins) :
261
    quartbins.append( [quart_min + (quart_width*i), quart_min + (quart_width*(i+1))] )
262
  newquartedges = sorted(list(set([ j for i in quartbins for j in i ])))[1:-1]
263

    
264
  # calculate energies of the new edges
265
  newquartedges_en = frac2en( newquartedges ) 
266

    
267
  # figure out list of detx/y bins
268
  detbins = []
269
  j = 0 
270
  while True :
271
    inner = j     * binwidth
272
    outer = (j+1) * binwidth
273
    if inner > binr :
274
      break
275
    detbins.append(    [    i for i in [inner,outer] ] )
276
    detbins.insert( 0, [ -1*i for i in [outer,inner] ] )
277
    j += 1
278

    
279
  # setup background dictionary
280
  print 'energy bins, ~%d events (%d%% of all events) per bin' % ( nev*(quartbins[0][1]-quartbins[0][0]), 100*(quartbins[0][1]-quartbins[0][0] ) )
281
  background = {}
282
  background['DETX_LO' ] = background['DETY_LO'] = [ det[0] for det in detbins ]
283
  background['DETX_HI' ] = background['DETY_HI'] = [ det[1] for det in detbins ]
284
  background['ENERG_LO'] = []
285
  background['ENERG_HI'] = []
286
  background['BGD'     ] = []
287

    
288
  # setup energy bins
289
  enbins = []
290
  for fracrange in quartbins :
291
    energybin = {}
292
    emin, emax = frac2en( fracrange )
293
    enbins.append( [emin, emax] )
294
    print '%5.3f - %5.3f TeV' % ( emin.TeV(), emax.TeV() )
295
    background['ENERG_LO'].append(emin)
296
    background['ENERG_HI'].append(emax)
297
    logmean = pow(10, ( emin.log10TeV() + emax.log10TeV() ) / 2.0 )
298
    
299
  # add backgrounds full of zeros
300
  ndet = len(detbins)
301
  nen  = len( enbins)
302
  bgd = numpy.zeros( (len(enbins),ndet,ndet), dtype='int32')
303

    
304
  # fill background 
305
  mindet = min( background['DETX_LO'] )
306
  def det2ind(val) :
307
    return int( ( val - mindet ) / binwidth )
308
  def en2ind(en) :
309
    for i, erange in enumerate( enbins ) :
310
      if en > erange[0] and en < erange[1] :
311
        return i
312
  livetime = 0
313
  for run in obs :
314
    livetime += run.livetime()
315
    for ev in run.events() :
316
      idetx = det2ind( ev.dir().detx() * gammalib.rad2deg )
317
      idety = det2ind( ev.dir().dety() * gammalib.rad2deg )
318
      ien   = en2ind(  ev.energy()                        )
319
      if type(ien) is type(3) :
320
        bgd[ien][idety][idetx] += 1
321
    
322

    
323
  # convert from array of ints (from numpy.zeros) to array of floats
324
  bgd = bgd.astype(numpy.float32)
325

    
326
  # set all bins with zeroes to something really small
327
  bgd[ bgd == 0 ] = zeroval
328
          
329
  # smooth background values via a gaussian kernel with a width of 'smooth' degrees
330
  bgd_smooth = []
331
  for i in range(nen) :
332
    
333
    if smoothmode == 'gauss' :
334
      # gaussian smoothing shamelessly stolen from stackoverflow
335
      # http://stackoverflow.com/questions/32155488/smoothing-a-2d-array-along-only-one-axis
336
      bgd_smooth.append( ndimage.gaussian_filter( bgd[i], smooth/binwidth ) ) # convert gaussw to units of '# bins'
337
      
338
    elif smoothmode == 'totalvariation' :
339
      presmoothsum = numpy.sum( bgd[i] )
340
      bgd_smooth.append( denoise_tv_chambolle_2d( bgd[i], weight=smooth   ) )
341
      postsmoothsum = numpy.sum( bgd_smooth[-1] )
342

    
343
      # normalize by the integral of all events
344
      bgd_smooth[-1] *= presmoothsum
345
      bgd_smooth[-1] /= postsmoothsum
346
      
347
    elif smoothmode == 'none' :
348
      bgd_smooth.append( bgd[i] )
349
      
350
    else :
351
      raise ValueError( "smoothmode is currently %s, must be one of ['gauss', 'totalvariation', 'none']" % repr(smoothmode) )
352

    
353
  # find min/max background values
354
  zmin = numpy.min( [bgd,bgd_smooth] )
355
  zmax = numpy.max( [bgd,bgd_smooth] )
356

    
357
  # apply normalization factor to account for exposure
358
  bin_sr = binwidth * binwidth * gammalib.deg2rad * gammalib.deg2rad
359
  bgd_norm = []
360
  for i in range(nen) :
361
    emin, emax = enbins[i]
362
    en_norm    = emax.MeV() - emin.MeV()
363
    normbin    = bin_sr * livetime * en_norm
364
    bgd_norm.append( bgd_smooth[i] / normbin )
365
  norm_min = numpy.min( bgd_norm )
366
  norm_max = numpy.max( bgd_norm )
367

    
368
  # plots of exposure-normalized bin values
369
  mevnorm = [ en[1].MeV() - en[0].MeV() for en in enbins ]
370
    
371
  # write background to a fits file
372
  background['BGD'] = bgd_norm
373
    
374
  # create temporary fits filename and write our fits table to it
375
  #fname = os.path.join( tempfile._get_default_tempdir(), next(tempfile._get_candidate_names())+'.fits' )
376
  background2fits( background, fname )  
377

    
378
  ret = {}
379
  ret['maxoffset' ] = maxoffset
380
  ret['enbins'    ] = enbins
381
  ret['detx'      ] = detbins
382
  ret['dety'      ] = detbins
383
  ret['bgd'       ] = bgd
384
  ret['bgd_smooth'] = bgd_smooth
385
  ret['bgd_norm'  ] = bgd_norm
386
  return ret
387

    
388
if __name__ == '__main__' :
389
  
390
  if len(sys.argv) == 1 :
391
    print 'Script for converting a GObservations xml file into a GCTABackground3D fits file.'
392
    print 'Usage is:'
393
    print ' $ %s xmlfile.xml outputbackground.fits' % __file__
394
    sys.exit(0)
395
  
396
  if len(sys.argv) != 3 :
397
    print 'Incorrect number of arguments.'
398
    print
399
    print 'Usage is:'
400
    print ' $ %s xmlfile.xml outputbackground.fits' % __file__
401
    sys.exit(1)
402
  
403
  xmlfile  = sys.argv[1]
404
  fitsfile = sys.argv[2]
405
  obs = gammalib.GObservations()
406
  obs.load( xmlfile )
407
  
408
  write_background_to_fits( obs, fitsfile )
409