Commit 172f60c6d077e8a011252df1d9612d8a582ec569

Authored by Jürgen Knödlseder
2 parents dc062fcd d74a1d66

Merge branch '1263-High-level-script' into integration

Conflicts:
	pyext/setup.py.in
pyext/ctools/__init__.py
1 # Import sub modules 1 # Import sub modules
2 from .tools import * 2 from .tools import *
3 import obsutils 3 import obsutils
  4 +import analysisutils
  5 +import plotutils
4 6
5 # Remove swig register stuff 7 # Remove swig register stuff
6 bad_entries = [entry for entry in list(locals()) 8 bad_entries = [entry for entry in list(locals())
@@ -12,4 +14,4 @@ for entry in bad_entries: @@ -12,4 +14,4 @@ for entry in bad_entries:
12 del tools 14 del tools
13 del gammalib 15 del gammalib
14 del bad_entries 16 del bad_entries
15 -del entry  
16 \ No newline at end of file 17 \ No newline at end of file
  18 +del entry
pyext/ctools/analysisutils.py 0 → 100644
  1 +# ==========================================================================
  2 +# This script provides a number of functions that are useful for CTA data
  3 +# analysis.
  4 +#
  5 +# Copyright (C) 2014 David Sanchez
  6 +#
  7 +# This program is free software: you can redistribute it and/or modify
  8 +# it under the terms of the GNU General Public License as published by
  9 +# the Free Software Foundation, either version 3 of the License, or
  10 +# (at your option) any later version.
  11 +#
  12 +# This program is distributed in the hope that it will be useful,
  13 +# but WITHOUT ANY WARRANTY; without even the implied warranty of
  14 +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15 +# GNU General Public License for more details.
  16 +#
  17 +# You should have received a copy of the GNU General Public License
  18 +# along with this program. If not, see <http://www.gnu.org/licenses/>.
  19 +#
  20 +# ==========================================================================
  21 +import ctools as ct
  22 +import gammalib as gl
  23 +from math import sqrt,log10,floor
  24 +import sys
  25 +
  26 +class base(object):
  27 + def __init__(self):
  28 + self.classname = self.__class__.__name__
  29 + self.errorcolor = "\033[31m"#red
  30 + self.infocolor = "\033[34m"#blue
  31 + self.warningcolor = "\033[33m"#yellow
  32 + self.successcolor = "\033[32m"#green
  33 + self.endcolor = "\033[0m"#reset
  34 + self.prependfunction = True
  35 + self.basemembers = ["classname","errorcolor","infocolor","warningcolor","successcolor","endcolor","prependfunction"]
  36 +
  37 + def error(self,message, functionname = ""):
  38 + printstring = ""
  39 + if functionname == "":
  40 + printstring = "\n"+self.errorcolor+"*** Error ["+self.classname+"]: "+message+" ***\n"+self.endcolor
  41 + else:
  42 + printstring = "\n"+self.errorcolor+"*** Error ["+self.classname+"::"+functionname+"]: "+message+" ***\n"+self.endcolor
  43 + sys.exit(printstring)
  44 +
  45 + def info(self,message,newline=True):
  46 + printstring = self.infocolor+"["+self.classname+"]: "+message+self.endcolor
  47 + if newline:
  48 + print printstring
  49 + else:
  50 + print self.infocolor+message+self.endcolor,
  51 + sys.stdout.flush()
  52 +
  53 + def warning(self,message,functionname = ""):
  54 + printstring = ""
  55 + if functionname == "":
  56 + printstring = self.warningcolor+"["+self.classname+"] Warning: "+message+self.endcolor
  57 + else:
  58 + printstring = self.warningcolor+"["+self.classname+"::"+functionname+"] Warning: "+message+self.endcolor
  59 + print printstring
  60 +
  61 + def success(self,message):
  62 + printstring = self.successcolor+"["+self.classname+"]: "+message+self.endcolor
  63 + print printstring
  64 +
  65 + def progress(self,message="."):
  66 + string = self.infocolor+message+self.endcolor
  67 + print string
  68 +
  69 +
  70 +class ResultsSorage(dict,base):
  71 + """class to store the results in the good format"""
  72 + def __init__(self,*arg,**kw):
  73 + super(ResultsSorage, self).__init__(*arg, **kw)
  74 + base.__init__(self)
  75 + self["dnde"] = {}
  76 + self["ulim_dnde"] = []
  77 + self["time"] = {}
  78 + self["ener"] = {}
  79 + self["TS"] = []
  80 + self["Iflux"] = {}
  81 +
  82 + self["time"]["tmin_value"] = []
  83 + self["time"]["tmax_value"] = []
  84 + self["time"]["unit"] = "sec"
  85 +
  86 + self["ener"]["value"] = []
  87 + self["ener"]["ed_value"] = []
  88 + self["ener"]["eu_value"] = []
  89 + self["ener"]["unit"] = "MeV"
  90 +
  91 + self["dnde"]["value"] = []
  92 + self["dnde"]["ed_value"] = []
  93 + self["dnde"]["eu_value"] = []
  94 + self["dnde"]["unit"] = "ph/cm2/s/MeV"
  95 +
  96 + self["Iflux"]["value"] = []
  97 + self["Iflux"]["ed_value"] = []
  98 + self["Iflux"]["eu_value"] = []
  99 + self["Iflux"]["unit"] = "ph/cm2/s"
  100 +
  101 + def Print(self):
  102 + self.success("Results are :")
  103 + for k in self.iterkeys():
  104 + self.info("Key name : "+k)
  105 + try :
  106 + for ki in self[k].iterkeys():
  107 + print "\t",ki,"\t",self[k][ki]
  108 + except:
  109 + print self[k]
  110 + print
  111 +
  112 +class Analyser(base):
  113 + def __init__(self):
  114 + super(Analyser,self).__init__()
  115 + self.info("Creating "+self.classname+" object")
  116 + self.m_obs = None
  117 + self.like = None
  118 +
  119 + self.m_ebinalg = "LOG"
  120 + self.m_eunit = "TeV"
  121 + self.m_enumbins = 10#nbins
  122 + self.m_usepnt = True # Use pointing for map centre
  123 + self.m_coordsys = "CEL"#coord
  124 + self.m_proj = "TAN"#proj
  125 + self.m_binned = False
  126 + self.m_edisp = False
  127 + self.m_stat = "POISSON"#stat
  128 +
  129 + self.m_source_set = False
  130 + self.m_files_set = False
  131 + self.m_time_set = False
  132 + self.m_energy_set = False
  133 + self.m_irfs_set = False
  134 + self.m_roi_set = False
  135 + self.m_xml_set = False
  136 +
  137 + def set_xml(self,xml):
  138 + self.m_xml = xml
  139 + ind = xml.find(".xml")
  140 + if not ind==-1:
  141 + self.m_xml_out = xml[:xml.find(".xml")]+"_results"+xml[xml.find(".xml"):]
  142 + self.m_xml_set = True
  143 +
  144 + def set_roi(self,roi):
  145 + self.m_roi = float(roi)
  146 + self.m_nxpix = int(self.m_roi*100) #nxpix
  147 + self.m_nypix = int(self.m_roi*100) #nypix
  148 + self.m_binsz = 0.05 #binsz
  149 + self.m_roi_set = True
  150 +
  151 + def set_irfs(self,caldb,irf):
  152 + self.m_caldb = caldb
  153 + self.m_irf = irf
  154 + self.m_irfs_set = True
  155 +
  156 + def set_Fitsfiles(self,evtfile,tag="CTA_analysis"):
  157 + self.m_evtfile = "event_selected_"+tag+".fits"
  158 + self.m_raw_evt = evtfile
  159 + self.m_cntfile = "countmap_"+tag+".fits"
  160 + self.m_files_set = True
  161 +
  162 + def set_source(self,name,ra,dec):
  163 + self.m_ra = ra
  164 + self.m_dec = dec
  165 + self.m_name = name
  166 + self.m_source_set = True
  167 +
  168 + def validate(self):
  169 + if not self.m_time_set :
  170 + self.error("Time range of the observations not set")
  171 +
  172 + if not self.m_files_set :
  173 + self.error("FITS files of the observations not set")
  174 +
  175 + if not self.m_source_set :
  176 + self.error("Source not set")
  177 +
  178 + if not self.m_energy_set :
  179 + self.error("Energy range of the observations not set")
  180 +
  181 + if not self.m_irfs_set :
  182 + self.error("IRFs to use not set")
  183 +
  184 + if not self.m_roi_set :
  185 + self.error("ROI to use not set")
  186 +
  187 + if not self.m_xml_set :
  188 + self.error("XML files for the sky model not set")
  189 +
  190 + def copy(self,analyse,tag="CTA_analysis_copy"):
  191 + self.set_xml(analyse.m_xml)
  192 + self.set_irfs(analyse.m_caldb,analyse.m_irf)
  193 + self.set_roi(analyse.m_roi)
  194 + self.set_time_boundary(analyse.m_tmin,analyse.m_tmax)
  195 + self.set_energy_boundary(analyse.m_emin,analyse.m_emax)
  196 + self.set_source(analyse.m_name,analyse.m_ra,analyse.m_dec)
  197 + self.set_Fitsfiles(analyse.m_evtfile,tag)
  198 +
  199 +
  200 + def set_obs(self,obs):
  201 + self.m_obs = obs #set the GObservation container
  202 +
  203 + def set_time_boundary(self,tmin,tmax):
  204 + """Set the start and stop time of the observation"""
  205 + self.m_tmin = float(tmin)
  206 + self.m_tmax = float(tmax)
  207 + self.m_time_set = True
  208 +
  209 + def set_energy_boundary(self,emin,emax):
  210 + """Set the energy bounds of the observation"""
  211 + self.m_emin = emin
  212 + self.m_emax = emax
  213 + ebounds = gl.GEbounds(gl.GEnergy(emin, self.m_eunit), \
  214 + gl.GEnergy(emax, self.m_eunit))
  215 +
  216 + if self.m_obs:
  217 + for obs in self.m_obs:
  218 + obs.events().ebounds(ebounds)
  219 + self.m_energy_set = True
  220 +
  221 + def ctselect(self):
  222 + # ctselect application and set parameters
  223 + self.info("Running ctselect to cut on events")
  224 + self.validate()
  225 + if self.m_obs:
  226 + filter = ct.ctselect(self.m_obs)
  227 + else:
  228 + filter = ct.ctselect()
  229 +
  230 + filter["infile"] = self.m_raw_evt
  231 + filter["outfile"] = self.m_evtfile
  232 + filter["usepnt"].boolean(self.m_usepnt) # Use pointing for map centre
  233 + filter["ra"] = self.m_ra
  234 + filter["dec"] = self.m_dec
  235 + filter["rad"] = self.m_roi
  236 + filter["tmin"] = self.m_tmin
  237 + filter["tmax"] = self.m_tmax
  238 + filter["emin"].real(self.m_emin)
  239 + filter["emax"].real(self.m_emax)
  240 + filter["expr"] = ""
  241 + filter.logFileOpen()
  242 +
  243 + filter.run()
  244 + if not(self.m_obs):
  245 + filter.save()
  246 +
  247 + if self.m_obs:
  248 + # Make a deep copy of the observation that will be returned
  249 + # (the ctbin object will go out of scope one the function is
  250 + # left)
  251 + self.m_obs = filter.obs().copy()
  252 +
  253 + def ctbin(self,log=False,debug=False):
  254 + # ctbin application and set parameters
  255 + self.validate()
  256 + self.info("Running ctbin to create count map")
  257 + if self.m_obs:
  258 + bin = ct.ctbin(self.m_obs)
  259 + else:
  260 + bin = ct.ctbin()
  261 + bin["evfile"] = self.m_evtfile
  262 + bin["outfile"] = self.m_cntfile
  263 + bin["ebinalg"].string(self.m_ebinalg)
  264 + bin["emin"].real(self.m_emin)
  265 + bin["emax"].real(self.m_emax)
  266 + Nbdecade = log10(self.m_emax)-log10(self.m_emin)#Compute the number of decade
  267 + bin["enumbins"].integer(int(self.m_enumbins*Nbdecade))
  268 + bin["usepnt"].boolean(self.m_usepnt) # Use pointing for map centre
  269 + bin["nxpix"].integer(self.m_nxpix)
  270 + bin["nypix"].integer(self.m_nypix)
  271 + bin["binsz"].real(self.m_binsz)
  272 + bin["coordsys"].string(self.m_coordsys)
  273 + bin["proj"].string(self.m_proj)
  274 +
  275 + # Optionally open the log file
  276 + if log:
  277 + bin.logFileOpen()
  278 +
  279 + # Optionally switch-on debugging model
  280 + if debug:
  281 + bin["debug"].boolean(True)
  282 +
  283 + # Run ctbin application. This will loop over all observations in
  284 + # the container and bin the events in counts maps
  285 + bin.execute()
  286 + if self.m_obs:
  287 + # Make a deep copy of the observation that will be returned
  288 + # (the ctbin object will go out of scope one the function is
  289 + # left)
  290 + self.m_obs = bin.obs().copy()
  291 +
  292 + def create_fit(self,log=False,debug=False):
  293 + self.validate()
  294 + # create ctlike instance with given parameters
  295 + self.info("Fitting Data using ctlike")
  296 + if self.m_obs:
  297 + self.like = ct.ctlike(self.m_obs)
  298 + else:
  299 + self.like = ct.ctlike()
  300 + if self.m_binned:
  301 + self.like["infile"] = self.m_cntfile
  302 + else:
  303 + self.like["infile"] = self.m_evtfile
  304 +
  305 + self.like["stat"].string(self.m_stat)
  306 + self.like["caldb"].string(self.m_caldb)
  307 + self.like["irf"].string(self.m_irf)
  308 + self.like["srcmdl"] = self.m_xml
  309 + self.like["edisp"].boolean(self.m_edisp)
  310 + self.like["outmdl"] = self.m_xml_out
  311 +
  312 + # Optionally open the log file
  313 + if log:
  314 + self.like.logFileOpen()
  315 + # Optionally switch-on debugging model
  316 + if debug:
  317 + self.like["debug"].boolean(True)
  318 +
  319 + def fit(self,log=False,debug=False):
  320 + self.validate()
  321 + if not(self.like):
  322 + self.warning("ctlike object not created, creating now")
  323 + self.create_fit(log,debug)
  324 +
  325 + # Run ctlike application.
  326 + self.like.run()
  327 + # Save the results in XML
  328 + self.like.save()
  329 + self.success("Fit performed")
  330 +
  331 + def PrintResults(self,srcname = ""):
  332 + self.info("Results of the Fit")
  333 + for m in self.like.obs().models():
  334 + if srcname == m.name() or srcname=="":
  335 + print "Model : "+m.name()
  336 + print m
  337 +
  338 + def GetSrcResuls(self,Res,srcname,E0=None,factor = 1e6,tmin=None,tmax=None):
  339 + """function to get and store the results of a fit"""
  340 + results = self.GetSrcParams(srcname)
  341 +
  342 + if 1:
  343 + # #Time
  344 + # if (tmin is not None) or (tmax is not None) :
  345 + # Res["time"]["tmin_value"].append(tmin)
  346 + # Res["time"]["tmax_value"].append(tmax)
  347 + # Res["time"]["unit"] = "sec"
  348 + # else:
  349 + Res["time"]["tmin_value"].append(self.m_tmin)
  350 + Res["time"]["tmax_value"].append(self.m_tmax)
  351 + Res["time"]["unit"] = "sec"
  352 +
  353 + #Energy
  354 + if (E0 is not None):
  355 + Res["ener"]["value"].append(E0*factor)
  356 + Res["ener"]["ed_value"].append((E0-self.m_emin)*factor)
  357 + Res["ener"]["eu_value"].append((self.m_emax-E0)*factor)
  358 + Res["ener"]["unit"] = "MeV"
  359 + else:
  360 + Res["ener"]["value"].append(sqrt(self.m_emin*self.m_emax))
  361 + Res["ener"]["ed_value"].append(-self.m_emin+sqrt(self.m_emin*self.m_emax))
  362 + Res["ener"]["eu_value"].append(self.m_emax-sqrt(self.m_emin*self.m_emax))
  363 + Res["ener"]["unit"] = self.m_eunit
  364 +
  365 + Res["Iflux"]["value"].append(results["flux"])
  366 + Res["Iflux"]["eu_value"].append(results["eflux"])
  367 + Res["Iflux"]["ed_value"].append(results["eflux"])
  368 + # Res["Iflux"]["unit"] = "unit"
  369 +
  370 + m = self.GetModel(srcname)
  371 + Res["dnde"]["value"].append(m.spectral()[0].value())
  372 + Res["dnde"]["eu_value"].append(m.spectral()[0].error())
  373 + Res["dnde"]["ed_value"].append(m.spectral()[0].error())
  374 +
  375 + Res["TS"].append(100)
  376 +
  377 + UL = UpperLimitComputer(self,srcname)
  378 + UL.run()
  379 + Res["ulim_dnde"].append(0)
  380 + # del UL
  381 + return Res
  382 +
  383 + def GetSrcParams(self,srcname):
  384 + results = {}
  385 + found = False
  386 + m = self.GetModel(srcname)
  387 + results["flux"] = m.spectral().flux(gl.GEnergy(self.m_emin,self.m_eunit),gl.GEnergy(self.m_emax,self.m_eunit))
  388 + results["eflux"] = m.spectral().eflux(gl.GEnergy(self.m_emin,self.m_eunit),gl.GEnergy(self.m_emax,self.m_eunit))
  389 + for par in m.spectral():
  390 + if par.is_free():
  391 + results[par.name()] = par.value()
  392 + results["e"+par.name()] = par.error()
  393 +
  394 + return results
  395 +
  396 + def GetModel(self,srcname):
  397 + found = False
  398 + for m in self.like.obs().models():
  399 + if m.name() == srcname:
  400 + return m
  401 + self.error("source "+srcname+" not in the source list")
  402 +
  403 +
  404 +def MakeEbin(analyse,nbins,srcname,binned = False):
  405 + #Store the results
  406 + Res = ResultsSorage()
  407 +
  408 + #check the source name, exit is the srcname is false
  409 + analyse.GetModel(srcname)
  410 +
  411 + #compute the conversion factor to have energy in MeV
  412 + Energy_list = {"MeV":1,"GeV":1e3,"TeV":1e6} #TODO
  413 + factor = Energy_list[analyse.m_eunit]
  414 +
  415 + for i in xrange(nbins):
  416 + obs_copy = analyse.like.obs().copy()
  417 + emin = pow(10,log10(analyse.m_emin) + (log10(analyse.m_emax)-log10(analyse.m_emin))/(nbins)*i)
  418 + emax = pow(10,log10(analyse.m_emin) + (log10(analyse.m_emax)-log10(analyse.m_emin))/(nbins)*(i+1))
  419 + E0 = pow(10, (log10(emin) + log10(emax)) / 2)
  420 + print "Making energy bin between %2.1e"%(emin)+analyse.m_eunit+" and %2.1e"%(emax)+analyse.m_eunit
  421 +
  422 + # change the PivotEnergy to E0
  423 + m = obs_copy.models()[srcname]
  424 + for par in m.spectral():
  425 + if par.name() == "PivotEnergy":
  426 + par.value(E0*factor)
  427 +
  428 + ana_bin = Analyser()
  429 +
  430 + #copy the obs object
  431 + ana_bin.set_obs(obs_copy)
  432 +
  433 + #copy the analyser parameters
  434 + ana_bin.copy(analyse)
  435 +
  436 + # Set energy boundaries
  437 + ana_bin.set_energy_boundary(emin,emax)
  438 + ana_bin.ctselect()
  439 +
  440 + # if binned:
  441 + # ana_bin.ctbin()
  442 +
  443 + # Create the fit object
  444 + ana_bin.create_fit(log=False,debug=False)
  445 +
  446 + # Run ctlike application.
  447 + ana_bin.like.run()
  448 +
  449 + #Retrive the results in the good format
  450 + Res = ana_bin.GetSrcResuls(Res,srcname,E0,factor)
  451 +
  452 + del ana_bin
  453 + return Res
  454 +
  455 +def MakeLC(analyse,nbins,srcname,binned = False):
  456 + #Store the results
  457 + Res = ResultsSorage()
  458 +
  459 + #check the source name, exit is the srcname is false
  460 + analyse.GetModel(srcname)
  461 +
  462 + for i in xrange(nbins):
  463 + obs_copy = analyse.like.obs().copy()
  464 + tmin = analyse.m_tmin + (analyse.m_tmax-analyse.m_tmin)/(nbins)*i
  465 + tmax = analyse.m_tmin + (analyse.m_tmax-analyse.m_tmin)/(nbins)*(i+1)
  466 + print "Making time bin between %2.1e"%(tmin)+" and %2.1e"%(tmax)
  467 +
  468 + ana_bin = Analyser()
  469 +
  470 + #copy the obs object
  471 + ana_bin.set_obs(obs_copy)
  472 +
  473 + #copy the analyser parameters
  474 + ana_bin.copy(analyse)
  475 +
  476 + # Set energy boundaries
  477 + ana_bin.set_time_boundary(tmin,tmax)
  478 + ana_bin.ctselect()
  479 +
  480 + # if binned:
  481 + # ana_bin.ctbin()
  482 +
  483 + # Create the fit object
  484 + ana_bin.create_fit(log=False,debug=False)
  485 +
  486 + # Run ctlike application.
  487 + ana_bin.like.run()
  488 +
  489 + #Retrive the results in the good format
  490 + Res = ana_bin.GetSrcResuls(Res,srcname,tmin=tmin,tmax=tmax)
  491 +
  492 + del ana_bin
  493 + return Res
  494 +
  495 +class UpperLimitComputer(base):
  496 + def __init__(self,analyser,srcname,parname = "Prefactor"):
  497 + super(UpperLimitComputer,self).__init__()
  498 + self.info("Creating "+self.classname+" object")
  499 + # self.like = analyser.like.copy()
  500 + obs = analyser.like.obs().copy()
  501 + self.like = ct.ctlike(obs)
  502 + self.succes = False
  503 +
  504 + # get spectral model
  505 + self.model = self.like.obs().models()[srcname]
  506 + self.spec = self.model.spectral()
  507 + self.parname = parname
  508 + self.bestloglike = analyser.like.opt().value()
  509 + self.dloglike = 3.84/2.0 #95% CL now
  510 +
  511 + def _bisec(self,a,b,tol = 1e-12):
  512 + """iterative function to find the roots"""
  513 + if (self._func(a)==0.):
  514 + return a
  515 + elif (self._func(b)==0):
  516 + return b
  517 + elif (self._func(b)*self._func(a)>0.):
  518 + self.warning("Finding root for upper limit calculation failed")
  519 + return 0
  520 + mid = (a+b)/2
  521 + err = abs(a-b)/2
  522 + fm = self._func(mid)
  523 + if (err<tol or self._func(mid)==0.):
  524 + self.succes = True
  525 + return mid
  526 + elif (fm*self._func(a)<0):
  527 + b = mid
  528 + else :
  529 + a = mid
  530 + return self._bisec(a,b)
  531 +
  532 + def _func(self,value):
  533 + """function to be minimized"""
  534 + # get parmeter value
  535 + val = value*self.spec[self.parname].scale()
  536 +
  537 + # ensure value to be above minimum boundary
  538 + if val <= self.spec[self.parname].min():
  539 + return 0.0-self.self.bestloglike-self.dloglike
  540 +
  541 + # set value
  542 + self.spec[self.parname].value(val)
  543 +
  544 + # Recalculate likelihood
  545 + self.like.run()
  546 + logl = self.like.opt().value()
  547 +
  548 + # Return likelihood difference
  549 + return (logl-self.bestloglike-self.dloglike)
  550 +
  551 + def run(self):
  552 +
  553 + # get spectral parameters and set start value
  554 + value = self.spec[self.parname].value()
  555 + error = self.spec[self.parname].error()
  556 + scale = self.spec[self.parname].scale()
  557 +
  558 + #Fix all parameters
  559 + for model in self.like.obs().models():
  560 + for par in model:
  561 + par.fix()
  562 +
  563 +
  564 + # set start value to 2 sigma above fit value
  565 + self.startpar = value / scale + 2* error/scale
  566 +
  567 + # set start (a) and (b) depend on the CL asked
  568 + a= value / scale + (self.dloglike/4.)*error/scale
  569 + b=value / scale + (self.dloglike*2)* error/scale
  570 + ul = self._bisec(a,b)
  571 +
  572 + self.ulimit = ul*self.spec[self.parname].scale()
  573 + # Test for convergence
  574 + if self.succes:
  575 + self.success("Upper limit of parameter \""+self.parname +"\" determined to "+str(self.ulimit))
pyext/ctools/plotutils.py
1 # ========================================================================== 1 # ==========================================================================
2 -# This script provides a number of functions that are useful for handling  
3 -# CTA observations. 2 +# This script provides a number of functions that are useful for plotting.
4 # 3 #
5 -# Copyright (C) 2014 Rolf Buehler 4 +# Copyright (C) 2014 David Sanchez
6 # 5 #
7 # This program is free software: you can redistribute it and/or modify 6 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by 7 # it under the terms of the GNU General Public License as published by
@@ -18,54 +17,405 @@ @@ -18,54 +17,405 @@
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 # 18 #
20 # ========================================================================== 19 # ==========================================================================
21 -import ctools  
22 -import gammalib 20 +import ctools as ct
  21 +import gammalib as gl
  22 +from math import log10,pow,sqrt
  23 +import os
  24 +
23 try: 25 try:
24 import matplotlib.pyplot as plt 26 import matplotlib.pyplot as plt
  27 + import matplotlib.gridspec as gridspec
25 has_matplotlib = True 28 has_matplotlib = True
26 except: 29 except:
27 has_matplotlib = False 30 has_matplotlib = False
  31 +
28 try: 32 try:
29 import aplpy 33 import aplpy
30 has_aplpy = True 34 has_aplpy = True
31 except: 35 except:
32 has_aplpy = False 36 has_aplpy = False
33 37
  38 +class options(object):
  39 + def __init__(self):
  40 + self.fmt = 'o'
  41 + self.color = BLUE
  42 + self.linecolor = 'black'
  43 + self.markercolor = self.color
  44 + self.tslim = 4.0
  45 + self.elinewidth = 2.0
  46 + self.linewidth = 2.0
  47 + self.linestyle = '--'
  48 + self.capsize = 5.0
  49 + self.markersize = 5.0
  50 + self.limlength = 0.4
  51 + self.label = "_nolegend_"
  52 +
  53 +import analysisutils
  54 +
  55 +class SpectralBase(object):
  56 + def __init__(self,sed=True,sed_factor=1.):
  57 + self.m_sed_factor = sed_factor
  58 + self.m_sed = sed
  59 +
  60 + def _convertSED(self,tab,energy):
  61 + """convert the a table storing dN/dE in sed with right units"""
  62 +
  63 + if len(tab) != len(energy):
  64 + self.error("input table and energy table does not match")
  65 + return
  66 + if self.m_sed:
  67 + for i in xrange(len(tab)):
  68 + tab[i] *= energy[i]**2*self.m_sed_factor
  69 + return tab
  70 +
  71 +BLUE="#337EF5"
  72 +GREY='grey'
  73 +GREEN = '#21851D'
  74 +RED = '#BD0B0B'
  75 +YELLOW = '#D4CF44'
  76 +ORANGE = '#D19534'
  77 +
  78 +
  79 +class residuals(analysisutils.base,options,SpectralBase):
  80 + def __init__(self,spectrum,datapoint,sed,sed_factor,eunit="TeV"):
  81 + super(residuals,self).__init__()
  82 + options.__init__(self)
  83 + SpectralBase.__init__(self,sed,sed_factor)
  84 +
  85 + # load the gammalib spectrum object
  86 + self.spectrum = spectrum
  87 + self.SetDataPoint(datapoint)
  88 +
  89 + self.xerrors = True
  90 + self.refUnit = eunit # reference unit for conversion
  91 +
  92 + def SetDataPoint(self,datapoint):
  93 + self.m_point_info = datapoint
  94 + self.Npt = len(self.m_point_info["dnde"]["value"])
  95 +
  96 + def _plt_residuals(self):
  97 + th_val = []
  98 + index = []
  99 + ed_y = []
  100 + eu_y = []
  101 + y = []
  102 +
  103 + Energy_list = {"MeV":1,"GeV":1e-3,"TeV":1e-6} #TODO
  104 + for i in range(self.Npt):
  105 + if self.m_point_info["TS"][i] >= self.tslim:
  106 + th_val.append(self.spectrum.eval(gl.GEnergy(self.m_point_info["ener"]["value"][i],self.m_point_info["ener"]["unit"]),gl.GTime(0)))
  107 + index.append(i)
  108 + else:
  109 + th_val.append(0)
  110 + if self.m_sed:
  111 + self._convertSED(th_val,self.m_point_info["ener"])
  112 + for i in index:
  113 + y.append((self.m_point_info["dnde"]["value"][i] - th_val[i]) / th_val[i])
  114 + ed_y.append(self.m_point_info["dnde"]["ed_value"][i]/th_val[i])
  115 + eu_y.append(self.m_point_info["dnde"]["eu_value"][i]/th_val[i])
  116 +
  117 + if self.xerrors:
  118 + plt.errorbar(self.m_point_info["ener"]["value"], y, xerr=[self.m_point_info["ener"]["ed_value"],self.m_point_info["ener"]["ed_value"]],yerr=[ed_y,eu_y],fmt=self.fmt,color=self.markercolor,elinewidth=self.elinewidth)
  119 + else:
  120 + plt.errorbar(self.m_point_info["ener"]["value"], y, yerr=[ed_y,eu_y],fmt=self.fmt,color=self.markercolor,elinewidth=self.elinewidth)
  121 +
  122 + plt.axhline(0.0,color=self.linecolor,lw=self.linewidth,ls=self.linestyle)
  123 +
  124 +
  125 +class ulimgraph(analysisutils.base,options,SpectralBase):
  126 + def __init__(self,datapoint,sed,sed_factor,eunit='TeV'):
  127 + super(ulimgraph,self).__init__()
  128 + #~ super(ulimgraph,self).__init__()
  129 + options.__init__(self)
  130 + SpectralBase.__init__(self,sed,sed_factor)
  131 +
  132 + self.xerrors=True
  133 + self.nlims = -1
  134 + self.refUnit = eunit
  135 + self.SetDataPoint(datapoint)
  136 +
  137 + def SetDataPoint(self,datapoint):
  138 + self.m_point_info = datapoint
  139 + self.Npt = len(self.m_point_info["dnde"]["value"])
  140 +
  141 + def _plt_points(self):
  142 + results = analysisutils.ResultsSorage()
  143 +
  144 + Energy_list = {"MeV":1,"GeV":1e-3,"TeV":1e-6} #TODO
  145 + for i in range(self.Npt):
  146 +
  147 + self.m_point_info["ener"]["value"][i]*=Energy_list[self.refUnit]
  148 + self.m_point_info["ener"]["ed_value"][i]*=Energy_list[self.refUnit]
  149 + self.m_point_info["ener"]["eu_value"][i]*=Energy_list[self.refUnit]
  150 + self.m_point_info["ener"]["unit"] = self.refUnit
  151 +
  152 + if self.m_point_info["TS"][i] >= self.tslim:
  153 +
  154 + if self.m_point_info["dnde"]["ed_value"][i]>self.m_point_info["dnde"]["value"][i]:
  155 + self.warning("\tflux error: "+str(self.m_point_info["dnde"]["ed_value"][i])+" is larger than flux value "+str(self.m_point_info["dnde"]["ed_value"][i]))
  156 + self.warning("\tTS value is however TS="+str(self.m_point_info["TS"][i])+", weird!")
  157 + else :
  158 + self.m_point_info["dnde"]["ed_value"][i] = 0
  159 + self.m_point_info["dnde"]["eu_value"][i] = 0
  160 +
  161 + if self.xerrors:
  162 + plt.errorbar(self.m_point_info["ener"]["value"], self.m_point_info["dnde"]["value"], xerr=[self.m_point_info["ener"]["ed_value"], self.m_point_info["ener"]["eu_value"]],yerr=[self.m_point_info["dnde"]["ed_value"], self.m_point_info["dnde"]["eu_value"]],fmt=self.fmt,color=self.markercolor,elinewidth=self.elinewidth)
  163 + else:
  164 + plt.errorbar(self.m_point_info["ener"]["value"], self.m_point_info["dnde"]["value"],yerr=[results["dnde"]["ed_value"],results["dnde"]["eu_value"]],fmt=self.fmt,color=self.markercolor,elinewidth=self.elinewidth)
  165 +
  166 + def _plt_limits(self):
  167 + ed_flux = []
  168 + eu_flux = []
  169 + flux = []
  170 + ener = []
  171 +
  172 + drawn_lims = 0
  173 + Energy_list = {"MeV":1,"GeV":1e-3,"TeV":1e-6} #TODO
  174 + for i in range(self.Npt):
  175 + if self.m_point_info["TS"][i] < self.tslim:
  176 + self.info("Plot an upper limit for E = "+self.m_point_info["ener"]["value"][i]+" with a TS = "+self.m_point_info["TS"][i])
  177 + if drawn_lims >= self.nlims and self.nlims!=-1:
  178 + break
  179 +
  180 + ed_flux.append(self.limlength*self.m_point_info["ulim_dnde"][i])
  181 + eu_flux.append(0.0)
  182 + flux.append(self.m_point_info["ulim_dnde"][i])
  183 + drawn_lims+=1
  184 +
  185 + # if self.xerrors:
  186 + # plt.errorbar(self.m_point_info["ener"]["value"], self.m_point_info["dnde"]["value"], xerr=[self.m_point_info["ener"]["ed_value"], self.m_point_info["ener"]["eu_value"]],yerr=[self.m_point_info["dnde"]["ed_value"],self.m_point_info["dnde"]["eu_value"]],fmt=self.fmt,markersize=0.0,elinewidth=self.elinewidth,lolims=True,capsize=self.capsize,label='_nolegend_',mfc = self.color,ecolor=self.color,ms =self.markersize)
  187 + # else:
  188 + plt.errorbar(ener, flux, yerr=[ed_flux, eu_flux],fmt=self.fmt,markersize=0.0,elinewidth=self.elinewidth,lolims=True,capsize=self.capsize,label='_nolegend_',mfc = self.color,ecolor=self.color,ms =self.markersize)
  189 +
  190 +
  191 +class LightCurvePlotter(analysisutils.base,options):
  192 + """ Class to plot the Light curve"""
  193 + def __init__(self,srcname,datapoint):
  194 + super(LightCurvePlotter,self).__init__()
  195 + options.__init__(self)
  196 + self.m_name = srcname
  197 + self.m_data = datapoint #data point in a Results Storage class
  198 +
  199 + def draw(self):
  200 + if not(has_matplotlib):
  201 + self.warning("matplotlib module not found, can draw")
  202 + return
  203 +
  204 + time = []
  205 + dtime = []
  206 + flux = []
  207 + dflux_ed = []
  208 + dflux_eu = []
  209 + for i in range(len(self.m_data["time"]["tmin_value"])):
  210 + time.append((self.m_data["time"]["tmin_value"][i]+self.m_data["time"]["tmax_value"][i])/2)
  211 + dtime.append((self.m_data["time"]["tmax_value"][i]-self.m_data["time"]["tmin_value"][i])/2)
  212 + flux.append(self.m_data["Iflux"]["value"][i])
  213 + dflux_eu.append(self.m_data["Iflux"]["eu_value"][i])
  214 + dflux_ed.append(self.m_data["Iflux"]["ed_value"][i])
  215 +
  216 + plt.figure("results",figsize=(12,7),edgecolor="w")
  217 + unit = self.m_data["time"]["unit"]
  218 + plt.xlabel("Time ["+ unit+"]",fontsize=15)
  219 + unit = self.m_data["Iflux"]["unit"]
  220 + plt.ylabel("E$^{2}$ dN/dE ["+unit+"]",fontsize=15)
  221 +
  222 + print time
  223 + print dtime
  224 + print flux
  225 + print dflux_ed
  226 + print dflux_eu
  227 + plt.errorbar(time,flux,xerr=[dtime,dtime],yerr=[dflux_ed,dflux_eu],fmt=self.fmt,color=self.markercolor,elinewidth=self.elinewidth)
  228 + plt.show()
  229 +
  230 +class SpectrumPlotter(analysisutils.base,SpectralBase):
  231 + """ Class to compute and plot the spectra (i.e best fit model and butterfly).
  232 + if data points are provided, only change applied is energy unit convertion to match the butterfly one
  233 + (i.e. no dN/dE to SED conversion).
  234 + It is currently assumed that the unit is MeV for the energy of the data point. this will be change
  235 + once the I/O container format will be defined."""
  236 + def __init__(self,srcname,like,datapoint,emin=0.1,emax=100,energy = "TeV",SED = True ,npt = 50):
  237 + super(SpectrumPlotter,self).__init__()
  238 +
  239 + self.m_ebound = [emin,emax]
  240 + self.m_name = srcname
  241 + self.m_like = like
  242 + self.m_spectral = like.obs().models()[srcname].spectral()
  243 + self.m_energy = []
  244 + self.m_flux = []
  245 + self.m_error = []
  246 + self.m_but = []
  247 + self.m_enebut = []
  248 + self.m_npt = npt
  249 + self.m_covar = gl.GMatrix(1,1)
  250 + self.covar()
  251 + #~ this part take care of the style : SED or differential flux
  252 + #~ in the case of SED, the y-unit is erg/cm2/s
  253 + self.m_sed = SED
  254 + self.eunit = energy
  255 + self.m_sed_factor = 1.
  256 + self._validateUnits()
  257 + SpectralBase.__init__(self,self.m_sed,self.m_sed_factor)
  258 +
  259 + #Assume MeV for the energy #TODO
  260 + self.points = ulimgraph(datapoint,self.m_sed,gl.MeV2erg,energy)
  261 + self.residuals = residuals(self.m_spectral,datapoint,self.m_sed,gl.MeV2erg,energy)
  262 +
  263 + def _validateUnits(self):
  264 + Energy_list = {"MeV":1,"GeV":1e3,"TeV":1e6}
  265 + if not(Energy_list.has_key(self.eunit)):
  266 + self.warning("Change energy unit to TeV")
  267 + self.eunit = "TeV"
  268 +
  269 + if self.m_sed:
  270 + self.m_sed_factor = gl.MeV2erg*Energy_list[self.eunit]**2
  271 +
  272 + def covar(self):
  273 + # Get covariance matrix if fit has converged
  274 + try:
  275 + fullcovar = self.m_like.obs().function().curvature().invert()
  276 + except:
  277 + self.warning("Covariance matrix not determined successfully")
  278 + return
  279 + #~ get the matrix element of the source of interest
  280 + #~ 1) get the right element indices
  281 + idx = 0
  282 + par_index_map = []
  283 + for m in self.m_like.obs().models() :
  284 + if m.name()!= self.m_name:
  285 + idx += m.size()
  286 + continue
  287 + idx += m.spatial().size()
  288 + for par in m.spectral():
  289 + if par.is_free():
  290 + par_index_map.append(idx)
  291 + idx += 1
  292 +
  293 + #~ 2) get the elemets and store them in a matrix
  294 + self.m_covar = gl.GMatrix(len(par_index_map),len(par_index_map))
  295 + i = 0
  296 + for xpar in par_index_map:
  297 + j = 0
  298 + for ypar in par_index_map:
  299 + self.m_covar[i,j] = fullcovar[xpar,ypar]
  300 + j += 1
  301 + i += 1
  302 + self.success("Covariance Matrix succesfuly computed")
  303 +
  304 + def _makespectrum(self):
  305 + for i in xrange(self.m_npt):
  306 + #Compute the energy
  307 + lene = log10(self.m_ebound[0])+log10(self.m_ebound[1])*i/(self.m_npt-1)
  308 + self.m_energy.append(pow(10,lene))
  309 + #Compute the flux
  310 + self.m_flux.append(self.m_spectral.eval(gl.GEnergy(pow(10,lene),self.eunit),gl.GTime(0)))
  311 +
  312 + #Compute the gradients
  313 + if self.m_covar.size() != 1:
  314 + self.m_spectral.eval_gradients(gl.GEnergy(pow(10,lene), self.eunit),gl.GTime(0))
  315 + #store them into a GVector
  316 + Derivative = gl.GVector(self.m_covar.size()/2)
  317 + j = 0
  318 + for par in self.m_spectral:
  319 + if par.is_free():
  320 + Derivative[j] = par.factor_gradient()
  321 + j += 1
  322 + #~ computed the error
  323 + self.m_error.append(sqrt(Derivative * (self.m_covar*Derivative)))
  324 +
  325 + #~ convert the flux in sed if asked
  326 + self.m_flux = self._convertSED(self.m_flux,self.m_energy)
  327 + if self.m_covar.size() != 1:
  328 + self.m_error = self._convertSED(self.m_error,self.m_energy)
  329 + self._makebutterfly() # make the butterfly
  330 + self.success("Spectrum computed")
  331 +
  332 + def _makebutterfly(self):
  333 + """make the butterfly by appending element in a table"""
  334 + for i in xrange(self.m_npt):
  335 + self.m_but.append(self.m_flux[i]+self.m_error[i])
  336 + self.m_enebut.append(self.m_energy[i])
  337 + for i in xrange(self.m_npt):
  338 + idx = self.m_npt-i-1
  339 + self.m_but.append(self.m_flux[idx]-self.m_error[idx])
  340 + self.m_enebut.append(self.m_energy[idx])
  341 + self.m_but.append(self.m_but[0])
  342 + self.m_enebut.append(self.m_enebut[0])
  343 +
  344 + def write(self):
  345 + self.warning("write function not yet implemented")
  346 +
  347 + def draw(self):
  348 + """plot the results, provide a file named ptfile for the flux points"""
  349 + gs = gridspec.GridSpec(2, 1,height_ratios=[3,1])
  350 + self._makespectrum()
  351 + self.write()
  352 +
  353 + if not(has_matplotlib):
  354 + self.warning("matplotlib module not found, can draw")
  355 + return
  356 +
  357 + plt.figure("results",figsize=(12,7),edgecolor="w")
  358 +
  359 + sb1 = plt.subplot(gs[0])
  360 + plt.loglog()
  361 + plt.xlabel("Energy ["+ self.eunit+"]",fontsize=15)
  362 +
  363 + if self.m_sed:
  364 + plt.ylabel("E$^{2}$ dN/dE [erg cm$^{2}$ s$^{-1}$]",fontsize=15)
  365 + else:
  366 + plt.ylabel("dN/dE [cm$^{-2}$ s$^{-1}$ "+ self.eunit+"$^{-1}$]",fontsize=15)
  367 +
  368 + plt.plot(self.m_energy,self.m_flux)
  369 + plt.plot(self.m_enebut,self.m_but)
  370 +
  371 + #~ plot the data points on top of the buterfly
  372 + self.points._plt_points()
  373 + self.points._plt_limits()
  374 +
  375 + #~ plot the residual points
  376 + sb2 = plt.subplot(gs[1])
  377 + plt.xscale('log')
  378 + self.residuals._plt_residuals()
  379 +
  380 + plt.show()
  381 +
34 382
35 # ============= # 383 # ============= #
36 # Plot skymaps # 384 # Plot skymaps #
37 # ============= # 385 # ============= #
38 -def plotskymaps(modelmap, countmap):  
39 - """  
40 - Plot the model map.  
41 -  
42 - Parameters:  
43 - modname - model input name  
44 - evtname - events input name  
45 - Keywords:  
46 - None  
47 - """  
48 - # Load model map  
49 - fig = plt.figure()  
50 - f1 = aplpy.FITSFigure(countmap, figure=fig, subplot=[0.1,0.1,0.35,0.8])  
51 - f1.set_tick_labels_font(size='small')  
52 - f1.set_axis_labels_font(size='small')  
53 - f1.show_colorscale()  
54 -  
55 - f1.tick_labels.set_yformat('dd:mm:ss')  
56 - f1.tick_labels.set_xformat('hh:mm:ss')  
57 - f1.axis_labels.set_xtext('Right Ascension (J2000)')  
58 - f1.axis_labels.set_ytext('Declination (J2000)')  
59 - f1.ticks.set_length(10.5, minor_factor=0.5)  
60 -  
61 - f2 = aplpy.FITSFigure(modelmap, figure=fig, subplot=[0.5,0.1,0.35,0.8])  
62 - f2.set_tick_labels_font(size='small')  
63 - f2.set_axis_labels_font(size='small')  
64 - f2.show_colorscale()  
65 -  
66 - f2.hide_yaxis_label()  
67 - f2.hide_ytick_labels()  
68 -  
69 - fig.canvas.draw()  
70 - fig.show()  
71 - 386 +class MapPlotter(analysisutils.base):
  387 + def __init__(self,modelmap, countmap):
  388 + super(MapPlotter,self).__init__()
  389 + self.modelmap = modelmap
  390 + self.countmap = countmap
  391 +
  392 + if not(has_aplpy) :
  393 + self.error("Module aplypy needed")
  394 + if not(has_matplotlib) :
  395 + self.error("Module matplotlib needed")
  396 +
  397 + def draw(self):
  398 + """ Plot the model map. """
  399 + # Load model map
  400 + fig = plt.figure()
  401 + f1 = aplpy.FITSFigure(self.countmap, figure=fig, subplot=[0.1,0.1,0.35,0.8])
  402 + f1.set_tick_labels_font(size='small')
  403 + f1.set_axis_labels_font(size='small')
  404 + f1.show_colorscale()
  405 +
  406 + f1.tick_labels.set_yformat('dd:mm:ss')
  407 + f1.tick_labels.set_xformat('hh:mm:ss')
  408 + f1.axis_labels.set_xtext('Right Ascension (J2000)')
  409 + f1.axis_labels.set_ytext('Declination (J2000)')
  410 + f1.ticks.set_length(10.5, minor_factor=0.5)
  411 +
  412 + f2 = aplpy.FITSFigure(self.modelmap, figure=fig, subplot=[0.5,0.1,0.35,0.8])
  413 + f2.set_tick_labels_font(size='small')
  414 + f2.set_axis_labels_font(size='small')
  415 + f2.show_colorscale()
  416 +
  417 + f2.hide_yaxis_label()
  418 + f2.hide_ytick_labels()
  419 +
  420 + fig.canvas.draw()
  421 + fig.show()
pyext/setup.py.in
@@ -121,7 +121,8 @@ setup(name=&#39;@PACKAGE_NAME@&#39;, @@ -121,7 +121,8 @@ setup(name=&#39;@PACKAGE_NAME@&#39;,
121 author='Juergen Knoedlseder', 121 author='Juergen Knoedlseder',
122 author_email='@PACKAGE_BUGREPORT@', 122 author_email='@PACKAGE_BUGREPORT@',
123 url='http://gammalib.sourceforge.net/', 123 url='http://gammalib.sourceforge.net/',
124 - py_modules=['ctools.tools', 'ctools.obsutils'], 124 + py_modules=['ctools.tools', 'ctools.obsutils', 'ctools.analysisutils',
  125 + 'ctools.plotutils'],
125 ext_modules=[Extension('ctools._tools', ['ctools/tools_wrap.cpp'], 126 ext_modules=[Extension('ctools._tools', ['ctools/tools_wrap.cpp'],
126 include_dirs=include_dirs, 127 include_dirs=include_dirs,
127 library_dirs=library_dirs, 128 library_dirs=library_dirs,