Commit 3b405d4c54436ab184bc1732ef5a3aecdd55fa95

Authored by Jürgen Knödlseder
2 parents 1166d886 2bf3699d

Merge branch 'add-cshessobs-to-build-xml-obs-files' into integration

scripts/Makefile.am
@@ -5,14 +5,14 @@ binscripts = cta_kb_bkgrates.py cta_root2irf.py cta_make_nodes.py \ @@ -5,14 +5,14 @@ binscripts = cta_kb_bkgrates.py cta_root2irf.py cta_make_nodes.py \
5 tsmerge.py 5 tsmerge.py
6 6
7 # Scripts to be distributed in pythondir 7 # Scripts to be distributed in pythondir
8 -pythonscripts = cspull.py cssens.py cstsdist.py cscaldb.py csresmap.py \ 8 +pythonscripts = cspull.py cssens.py cstsdist.py cscaldb.py csresmap.py cshessobs.py \
9 csobsdef.py csspec.py \ 9 csobsdef.py csspec.py \
10 lsmodel.py \ 10 lsmodel.py \
11 obsutils.py 11 obsutils.py
12 12
13 # Scripts for which symbolic links will be created in bindir 13 # Scripts for which symbolic links will be created in bindir
14 linkscripts = cspull cssens cstsdist cscaldb csresmap \ 14 linkscripts = cspull cssens cstsdist cscaldb csresmap \
15 - csobsdef csspec \ 15 + csobsdef csspec cshessobs\
16 lsmodel 16 lsmodel
17 17
18 # Distribute scripts 18 # Distribute scripts
scripts/cshessobs.py 0 → 100755
  1 +#!/usr/bin/env python
  2 +# ==========================================================================
  3 +# Generation of an H.E.S.S. observation definition file.
  4 +#
  5 +# Copyright (C) 2015 Michael Mayer
  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 gammalib
  22 +import ctools
  23 +import sys
  24 +import os
  25 +import glob
  26 +
  27 +
  28 +# =============== #
  29 +# cshessobs class #
  30 +# =============== #
  31 +class cshessobs(ctools.cscript):
  32 + """
  33 + This class implements the creation of a observation xml file for HESS
  34 + data analysis. This class is dedicated for use inside the HESS Collaboration,
  35 + i.e. it can only be used if you have access to HESS data in FITS format.
  36 + The HESSFITS environment has to be set correctly to use this script
  37 + Please see the internal documentation for this purpose:
  38 + https://hess-confluence.desy.de/confluence/display/HESS/HESS+Open+Source+Tools+-+HESS+data+and+IRFs+in+FITS+format
  39 + """
  40 + def __init__(self, *argv):
  41 + """
  42 + Constructor.
  43 + """
  44 + # Set name
  45 + self.name = "cshessobs"
  46 + self.version = "0.1.0"
  47 +
  48 + # Check on existence of HESSFITS environment variable
  49 + try:
  50 + self.datapath = os.environ["HESSFITS"]
  51 + except KeyError:
  52 + raise RuntimeError("Environment variable \"HESSFITS\" not set. " \
  53 + "Please set the environment variable \"HESSFITS\" to use this script")
  54 +
  55 + # Initialise some members
  56 + self.datapath = ""
  57 + self.inmodels = None
  58 + self.outobs = "obs.xml"
  59 + self.nodes = 1
  60 +
  61 + # Make sure that parfile exists
  62 + file = self.parfile()
  63 +
  64 + # Initialise application
  65 + if len(argv) == 0:
  66 + ctools.cscript.__init__(self, self.name, self.version)
  67 + elif len(argv) ==1:
  68 + ctools.cscript.__init__(self, self.name, self.version, *argv)
  69 + else:
  70 + raise TypeError("Invalid number of arguments given.")
  71 +
  72 + # Set logger properties
  73 + self.log_header()
  74 + self.log.date(True)
  75 +
  76 + # Return
  77 + return
  78 +
  79 + def __del__(self):
  80 + """
  81 + Destructor.
  82 + """
  83 + # Return
  84 + return
  85 +
  86 + def parfile(self):
  87 + """
  88 + Check if parfile exists. If parfile does not exist then create a
  89 + default parfile. This kluge avoids shipping the cscript with a parfile.
  90 + """
  91 +
  92 + # Set parfile name
  93 + parfile = self.name+".par"
  94 +
  95 + try:
  96 + pars = gammalib.GApplicationPars(parfile)
  97 + except:
  98 + # Signal that parfile was not found
  99 + print("Parfile \""+parfile+"\" not found. Create default parfile.")
  100 +
  101 + # Check for folders and possible/available configurations
  102 + chains = os.listdir(self.datapath)
  103 +
  104 + # Initialise folder dictionary
  105 + folders = {}
  106 +
  107 + # Loop over possible analysis chains
  108 + for chain in chains:
  109 +
  110 + # Omit hidden files/folders
  111 + if chain[0]=="." or not os.path.isdir(self.datapath+"/"+chain):
  112 + continue
  113 +
  114 + # Initialise dictionary per chain
  115 + folders[chain] = {}
  116 +
  117 + # Read available DST versions
  118 + dstversions = os.listdir(self.datapath+"/"+chain)
  119 +
  120 + # Loop over DST versions
  121 + for dstver in dstversions:
  122 +
  123 + # Omit hidden files/folders
  124 + if dstver[0] == "." or not os.path.isdir(self.datapath+"/"+chain+"/"+dstver):
  125 + continue
  126 +
  127 + # Initialise available cut configurations
  128 + folders[chain][dstver] = []
  129 + configs = os.listdir(self.datapath+"/"+chain+"/"+dstver)
  130 +
  131 + # Loop over configurations
  132 + for config in configs:
  133 +
  134 + # Omit hidden files/folders
  135 + if config[0] == "." or not os.path.isdir(self.datapath+"/"+chain+"/"+dstver+"/"+config):
  136 + continue
  137 +
  138 + # Append configuration to corresponding dictionary
  139 + folders[chain][dstver].append(config)
  140 +
  141 + # Check for appropriate folder structure
  142 + checked_folders = self.check_structure(folders)
  143 + if not len(checked_folders):
  144 + raise KeyError("Appropriate folder structure not found in $HESSFITS. The correct structure can be found on HESS internal documentation")
  145 +
  146 + # Create default parfile
  147 + pars = gammalib.GApplicationPars()
  148 +
  149 + # Create parameters for each available config
  150 + avchains = ""
  151 + for chain in checked_folders:
  152 + avchains += chain +"|"
  153 + avdsts = ""
  154 + for dstver in checked_folders[chain]:
  155 + avdsts+=dstver+"|"
  156 + avconfigs = ""
  157 + for config in checked_folders[chain][dstver]:
  158 + avconfigs += config+"|"
  159 + pars.append(gammalib.GApplicationPar(chain+"_"+dstver+"_config","s","a",checked_folders[chain][dstver][0],avconfigs[:-1],"","Analysis configuration"))
  160 + pars.append(gammalib.GApplicationPar(chain+"_dstver","s","a",checked_folders[chain].keys()[0],avdsts[:-1],"","DST version"))
  161 + pars.append(gammalib.GApplicationPar("chain","s","a",checked_folders.keys()[0],avchains[:-1],"","Analysis chain"))
  162 + pars.append(gammalib.GApplicationPar("runlistfile","f","a","runlist.lis","","","Runlist file"))
  163 + pars.append(gammalib.GApplicationPar("inmodel","f","h","NONE","","","Input model XML file (optional)"))
  164 + pars.append(gammalib.GApplicationPar("outobs","f","a","obs.xml","","","Observation XML outfile"))
  165 + pars.append(gammalib.GApplicationPar("outmodel","f","a","bgmodels.xml","","","Output model XML file"))
  166 + pars.append(gammalib.GApplicationPar("datapath","s","h",self.datapath,"","","Data directoy"))
  167 + pars.append(gammalib.GApplicationPar("nodes","i","h","1","","","Number of free parameters per background model"))
  168 + pars.append(gammalib.GApplicationPar("usetrig","b","h","yes","yes|no","","Pre-estimate background normalisation? Recommended for binned analysis"))
  169 + pars.append_standard()
  170 + pars.save(parfile)
  171 +
  172 + # Return
  173 + return
  174 +
  175 + def get_parameters(self):
  176 + """
  177 + Get parameters from parfile and setup the observation.
  178 + """
  179 + # Get parameters
  180 + self.runlistfile = self["runlistfile"].filename()
  181 +
  182 + # Read models if user input
  183 + self.inmodel = self["inmodel"].filename()
  184 + if not (self.inmodel == "NONE" or self.inmodel == ""):
  185 + self.inmodels = gammalib.GModels(self.inmodel)
  186 +
  187 + # Observation outfile
  188 + self.outobs = self["outobs"].filename()
  189 +
  190 + # Output model file
  191 + self.outmodel = self["outmodel"].filename()
  192 +
  193 + # Read hidden parameters
  194 + self.nodes = self["nodes"].integer()
  195 + self.usetrig = self["usetrig"].boolean()
  196 + self.datapath = self["datapath"].string()
  197 +
  198 + # Read run configuration parameters
  199 + self.chain = self["chain"].string()
  200 + self.dstver = self[self.chain+"_dstver"].string()
  201 + self.config = self[self.chain+"_"+self.dstver+"_config"].string()
  202 +
  203 + # Build directory name prefix
  204 + self.directory = self.datapath+"/"
  205 + self.directory += self.chain +"/"
  206 + self.directory += self.dstver +"/"
  207 + self.directory += self.config +"/"
  208 +
  209 + if not os.path.isdir(self.directory):
  210 + raise gammalib.Exception.runtime_error("Folder structure not found. Please check your HESSFITS environment variable")
  211 +
  212 + self.m_log = False # Logging in client tools
  213 + self.m_debug = False # Debugging in client tools
  214 + self.m_clobber = self["clobber"].boolean()
  215 +
  216 + # Return
  217 + return
  218 +
  219 + def check_structure(self,folders):
  220 +
  221 + # Initialise valid folders
  222 + newfolders = {}
  223 +
  224 + # Check if we have found possible chains
  225 + if len(folders):
  226 +
  227 + # Loop over chains
  228 + for chain in folders:
  229 +
  230 + # Loop over dst versions
  231 + for dst in folders[chain]:
  232 +
  233 + # Loop over configs:
  234 + for config in folders[chain][dst]:
  235 +
  236 + # check if config is valid
  237 + path = self.datapath+"/"+chain+"/"+dst+"/"+config
  238 + if os.path.isdir(path) and len(glob.glob(path+"/run*")):
  239 + if not newfolders.has_key(chain):
  240 + newfolders[chain] = {}
  241 + if not newfolders[chain].has_key(dst):
  242 + newfolders[chain][dst] = []
  243 + newfolders[chain][dst].append(config)
  244 +
  245 + # Return valid configs
  246 + return newfolders
  247 +
  248 + # Create instrument background model
  249 + def hess_inst_background(self,run,aefffile,trgrate=0.0):
  250 +
  251 + # Initialise return spectrum
  252 + spec = 0
  253 +
  254 + # Initialise default value
  255 + value = 1.0
  256 +
  257 + if self.usetrig and trgrate > 0.0:
  258 + # Use value from lookup
  259 + value = 0.00451521513379 * trgrate + 0.162316854264
  260 +
  261 + if self.nodes <= 1:
  262 +
  263 + # Use a constant scaling function
  264 + spec = gammalib.GModelSpectralConst()
  265 + spec["Value"].min(0.1)
  266 + spec["Value"].max(10)
  267 + spec["Value"].value(value)
  268 + if self.nodes < 1:
  269 + spec["Value"].fix()
  270 + else:
  271 + spec["Value"].free()
  272 +
  273 + elif self.nodes == 2:
  274 +
  275 + # User a power law scaling function
  276 + e = gammalib.GEnergy(1.0,"TeV")
  277 + spec = gammalib.GModelSpectralPlaw(1.0,0.0,e)
  278 + spec[0].free()
  279 + spec[0].min(0.01)
  280 + spec[0].max(10.0)
  281 + spec[0].scale(1.0)
  282 + spec[0].value(value)
  283 + spec[1].free()
  284 + spec[1].min(-2.0)
  285 + spec[1].max(2.0)
  286 + spec[1].scale(1.0)
  287 + else:
  288 +
  289 + # Use several energy-dependent scaling factors
  290 + fits = gammalib.GFits(aefffile)
  291 + emin = fits["EFFECTIVE AREA"].real("LO_THRES")
  292 + emax = fits["EFFECTIVE AREA"].real("HI_THRES")
  293 + fits.close()
  294 + spec = gammalib.GModelSpectralNodes()
  295 + bounds = gammalib.GEbounds(self.nodes,gammalib.GEnergy(emin,"TeV"),gammalib.GEnergy(emax,"TeV"),True)
  296 + for i in range(bounds.size()):
  297 + spec.append(bounds.elogmean(i),value)
  298 + for par in spec:
  299 + if "Energy" in par.name():
  300 + par.fix()
  301 + elif "Intensity" in par.name():
  302 + par.min(0.01)
  303 + par.max(10.0)
  304 +
  305 + # Create background model instance
  306 + bck = gammalib.GCTAModelIrfBackground(spec)
  307 +
  308 + # Assign specific run id
  309 + bck.ids(str(int(run)))
  310 +
  311 + # Assign instrument
  312 + bck.instruments("HESS")
  313 +
  314 + # Set name (arbitrary)
  315 + bck.name("bkg_"+run)
  316 +
  317 + # Turn off TS calculation for background model
  318 + bck.tscalc(False)
  319 +
  320 + # Return model
  321 + return bck
  322 +
  323 +
  324 + def execute(self):
  325 + """
  326 + Execute the script.
  327 + """
  328 + # Run the script
  329 + self.run()
  330 +
  331 + # Save residual map
  332 + self.save(self.outobs, self.m_clobber)
  333 +
  334 + # Return
  335 + return
  336 +
  337 + # Get runfolder from runnumber (subject to defined folder structure)
  338 + def get_runfolder(self,runnr):
  339 +
  340 + start = runnr[:-2]+"00"
  341 + if not int(runnr[-3]) % 2 == 0:
  342 + start = str(int(start)-100)
  343 + if len(start) == 5:
  344 + start = "0"+start
  345 + int_end = int(start)+199
  346 + end = str(int_end)
  347 + if len(end) == 5:
  348 + end = "0"+ end
  349 +
  350 + runfold = runnr
  351 + if len(runfold) == 5:
  352 + runfold = "0"+runfold
  353 +
  354 + return self.directory+"/run"+start+"-"+end+"/run"+runfold+"/"
  355 +
  356 + def run(self):
  357 + """
  358 + Run the script.
  359 + """
  360 + # Switch screen logging on in debug mode
  361 + if self.logDebug():
  362 + self.log.cout(True)
  363 +
  364 + # Get parameters
  365 + self.get_parameters()
  366 +
  367 + # Write input parameters into logger
  368 + if self.logTerse():
  369 + self.log_parameters()
  370 + self.log("\n")
  371 +
  372 + # Initialise empty models
  373 + self.models = gammalib.GModels()
  374 +
  375 + # Initialise empty xml file and append an observation list
  376 + self.xml = gammalib.GXml()
  377 + self.xml.append(gammalib.GXmlElement("observation_list title=\"observation list\""));
  378 + lib = self.xml.element("observation_list", 0);
  379 +
  380 + # Read runlist from file
  381 + runlist = []
  382 + runfile = open(self.runlistfile)
  383 + for line in runfile.readlines():
  384 + if len(line) == 0:
  385 + continue
  386 + if line[0] == '#':
  387 + continue
  388 + if len(line.split()) > 0:
  389 + runlist.append(line.split()[0])
  390 + else:
  391 + runlist.append('')
  392 + runfile.close()
  393 +
  394 + # Loop over runs
  395 + # Log output
  396 + if self.logTerse():
  397 + self.log("\n")
  398 + self.log.header1("Looping over runs")
  399 +
  400 + for runnr in runlist:
  401 +
  402 + # Unify run number string length
  403 + run = runnr
  404 + if len(run) == 5:
  405 + run = "0"+run
  406 +
  407 + # Get folder in which run files should be located
  408 + runfolder = self.get_runfolder(run)
  409 +
  410 + # construct filenames
  411 + eventfile = runfolder + "hess_events_"+run+".fits.gz"
  412 + aefffile = runfolder + "hess_aeff_2d_"+run+".fits.gz"
  413 + psffile = runfolder + "hess_psf_king_"+run+".fits.gz"
  414 + edispfile = ""#runfolder + "hess_edisp_2d_"+run+".fits.gz"
  415 + bgfile = runfolder + "hess_bkg_offruns_"+run+".fits.gz"
  416 +
  417 + # Check for existence of files
  418 + msg = ""
  419 + skip = False
  420 + if not os.path.isdir(runfolder):
  421 + msg = "Run "+str(int(run))+" does not exist in FITS format"
  422 + skip = True
  423 + elif not os.path.isfile(eventfile):
  424 + msg = "Run "+str(int(run))+" has no eventfile - Run is skipped"
  425 + skip = True
  426 + elif not os.path.isfile(aefffile):
  427 + msg = "Run "+str(int(run))+" has no effective area - Run is skipped"
  428 + skip = True
  429 + elif not os.path.isfile(psffile):
  430 + msg = "Run "+str(int(run))+" has no PSF - Run is skipped"
  431 + skip = True
  432 + elif not os.path.isfile(bgfile):
  433 + msg = "Run "+str(int(run))+" has no background - Run is skipped"
  434 + skip = True
  435 + else:
  436 + msg = "Adding run "+str(int(run))
  437 +
  438 + # Log ouput and skip run if not available
  439 + if self.logTerse():
  440 + self.log(msg+"\n")
  441 + if skip:
  442 + continue
  443 + if self.logExplicit():
  444 + self.log(" Event file: "+eventfile+"\n")
  445 + self.log(" Effective area: "+aefffile+"\n")
  446 + self.log(" Point spread function: "+psffile+"\n")
  447 + self.log(" Energy dispersion: "+edispfile+"\n")
  448 + self.log(" Background file: "+bgfile+"\n")
  449 +
  450 + # Open fits file to determine the observation name
  451 + fits = gammalib.GFits(eventfile)
  452 + object_name = fits["EVENTS"].string("OBJECT")
  453 + trgrate = fits["EVENTS"].real("ZTRGRATE")
  454 + fits.close()
  455 +
  456 + # Append observation to XML and set attributes
  457 + obs = lib.append("observation");
  458 + obs.attribute("name", object_name);
  459 + obs.attribute("id", str(int(run)));
  460 + obs.attribute("instrument", "HESS");
  461 +
  462 + # Append event file
  463 + par = gammalib.GXmlElement("parameter name=\"EventList\"")
  464 + par.attribute("file", eventfile);
  465 + obs.append(par)
  466 +
  467 + # Append effective area
  468 + aeff = gammalib.GXmlElement("parameter name=\"EffectiveArea\"")
  469 + aeff.attribute("file",aefffile)
  470 +
  471 + # Append PSF
  472 + psf = gammalib.GXmlElement("parameter name=\"PointSpreadFunction\"")
  473 + psf.attribute("file",psffile)
  474 +
  475 + # Append energy dispersion
  476 + edisp = gammalib.GXmlElement("parameter name=\"EnergyDispersion\"")
  477 + edisp.attribute("file",edispfile)
  478 +
  479 + # Append background
  480 + bck = gammalib.GXmlElement("parameter name=\"Background\"")
  481 + bck.attribute("file",bgfile)
  482 +
  483 + # assign events and IRFs to observation
  484 + obs.append(aeff)
  485 + obs.append(psf)
  486 + obs.append(edisp)
  487 + obs.append(bck)
  488 +
  489 + # Append instrumental background model
  490 + self.models.append(self.hess_inst_background(run,aefffile,trgrate))
  491 +
  492 + # Append models provided by 'inmodels' of necessary
  493 + if not self.inmodels == None:
  494 + if self.logTerse():
  495 + self.log("\n")
  496 + self.log.header1("Appending models")
  497 +
  498 + # Loop over input models
  499 + for model in self.inmodels:
  500 + if self.logTerse():
  501 + self.log.header3("Adding model \""+model.name()+"\"")
  502 + self.models.append(model)
  503 +
  504 + # Return
  505 + return
  506 +
  507 + def save(self,outfile,clobber):
  508 +
  509 + # Save observation XML file
  510 + self.xml.save(outfile)
  511 +
  512 + # Save model XML file
  513 + self.models.save(self.outmodel)
  514 +
  515 +
  516 +# ======================== #
  517 +# Main routine entry point #
  518 +# ======================== #
  519 +if __name__ == '__main__':
  520 + """
  521 + Generates observation file.
  522 + """
  523 + # Create instance of application
  524 + app = cshessobs(sys.argv)
  525 +
  526 + # Open logfile
  527 + app.logFileOpen()
  528 +
  529 + # Execute application
  530 + app.execute()
  531 +
scripts/csobsdef.py
@@ -202,8 +202,7 @@ class csobsdef(ctools.cscript): @@ -202,8 +202,7 @@ class csobsdef(ctools.cscript):
202 202
203 # Throw an exception is there is no header information 203 # Throw an exception is there is no header information
204 if pntdef.nrows() < 1: 204 if pntdef.nrows() < 1:
205 - raise gammalib.GException.invalid_value(self.name,  
206 - "No header found in pointing definition file.") 205 + raise RuntimeError("No header found in pointing definition file.")
207 206
208 # Clear observation container 207 # Clear observation container
209 self.obs.clear() 208 self.obs.clear()