Commit 74c18e2e98bd33d7ef0e57c5845c596e7b41cf32

Authored by Jürgen Knödlseder
2 parents 5e51f658 0b84af85

Merge branch 'integration' into devel

.gitignore
@@ -66,6 +66,7 @@ src/ctulimit/ctulimit @@ -66,6 +66,7 @@ src/ctulimit/ctulimit
66 src/cterror/cterror 66 src/cterror/cterror
67 test/pfiles/ 67 test/pfiles/
68 test/caldb/ 68 test/caldb/
  69 +test/iactdata_test
69 test/*.fits 70 test/*.fits
70 test/*.dat 71 test/*.dat
71 test/*.xml 72 test/*.xml
ChangeLog
1 -2016-01-20 Juergen Knoedlseder <jurgen.knodlseder@irap.omp.eu> 1 +2016-02-11 Juergen Knoedlseder <jurgen.knodlseder@irap.omp.eu>
2 2
3 * Version 1.1.0 released 3 * Version 1.1.0 released
4 ======================== 4 ========================
5 5
  6 + Add csiactcopy script (#1661)
6 ctselect allows usage of non-default events and GTI extensions (#1598) 7 ctselect allows usage of non-default events and GTI extensions (#1598)
7 8
8 9
@@ -2,7 +2,7 @@ New Features and Important Changes in ctools 1.1.0 @@ -2,7 +2,7 @@ New Features and Important Changes in ctools 1.1.0
2 2
3 Author(s) : Juergen Knoedlseder 3 Author(s) : Juergen Knoedlseder
4 4
5 -20 January 2016 5 +11 February 2016
6 6
7 7
8 1. Introduction 8 1. Introduction
@@ -127,7 +127,8 @@ None @@ -127,7 +127,8 @@ None
127 127
128 19. cscripts 128 19. cscripts
129 ------------ 129 ------------
130 -None 130 +Added the csiactcopy script that copies IACT data from a remote machine to
  131 +the user's machine (#1661).
131 132
132 133
133 20. Examples 134 20. Examples
cscripts/Makefile.am
@@ -5,14 +5,14 @@ pythonscripts = __init__.py \ @@ -5,14 +5,14 @@ pythonscripts = __init__.py \
5 cspull.py cssens.py cstsdist.py cscaldb.py csresmap.py \ 5 cspull.py cssens.py cstsdist.py cscaldb.py csresmap.py \
6 csiactobs.py csiactdata.py csobsdef.py csspec.py cslightcrv.py csobsinfo.py \ 6 csiactobs.py csiactdata.py csobsdef.py csspec.py cslightcrv.py csobsinfo.py \
7 csinfo.py csmodelinfo.py csmodelmerge.py cstsmapmerge.py \ 7 csinfo.py csmodelinfo.py csmodelmerge.py cstsmapmerge.py \
8 - csobs2caldb.py csfindobs.py \ 8 + csobs2caldb.py csfindobs.py csiactcopy.py \
9 obsutils.py 9 obsutils.py
10 10
11 # Scripts for which symbolic links will be created in bindir 11 # Scripts for which symbolic links will be created in bindir
12 linkscripts = cspull cssens cstsdist cscaldb csresmap \ 12 linkscripts = cspull cssens cstsdist cscaldb csresmap \
13 csiactobs csiactdata csobsdef csspec cslightcrv csobsinfo \ 13 csiactobs csiactdata csobsdef csspec cslightcrv csobsinfo \
14 csinfo csmodelinfo csmodelmerge cstsmapmerge \ 14 csinfo csmodelinfo csmodelmerge cstsmapmerge \
15 - csobs2caldb csfindobs 15 + csobs2caldb csfindobs csiactcopy
16 16
17 # Distribute scripts 17 # Distribute scripts
18 EXTRA_DIST = $(binscripts) $(pythonscripts) 18 EXTRA_DIST = $(binscripts) $(pythonscripts)
cscripts/__init__.py.in
@@ -39,6 +39,7 @@ __all__ = [ @@ -39,6 +39,7 @@ __all__ = [
39 "csfindobs", 39 "csfindobs",
40 "csiactobs", 40 "csiactobs",
41 "csiactdata", 41 "csiactdata",
  42 + "csiactcopy",
42 "obsutils", 43 "obsutils",
43 "test", 44 "test",
44 ] 45 ]
@@ -63,6 +64,7 @@ from .cstsmapmerge import cstsmapmerge @@ -63,6 +64,7 @@ from .cstsmapmerge import cstsmapmerge
63 from .csfindobs import csfindobs 64 from .csfindobs import csfindobs
64 from .csiactobs import csiactobs 65 from .csiactobs import csiactobs
65 from .csiactdata import csiactdata 66 from .csiactdata import csiactdata
  67 +from .csiactcopy import csiactcopy
66 from . import obsutils 68 from . import obsutils
67 69
68 # Add test function 70 # Add test function
cscripts/csiactcopy.py 0 → 100755
  1 +#!/usr/bin/env python
  2 +# ==========================================================================
  3 +# Download IACT data from remote machine
  4 +#
  5 +# Copyright (C) 2016 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 json
  26 +import shutil
  27 +
  28 +# ================ #
  29 +# csiactdata class #
  30 +# ================ #
  31 +class csiactcopy(ctools.cscript):
  32 + """
  33 + This script copies IACT data from one location to another.
  34 + It can take a list of observation IDs to allow the download specific
  35 + observations. Index files get merged and updated accordingly.
  36 + """
  37 + def __init__(self, *argv):
  38 + """
  39 + Constructor.
  40 + """
  41 + # Set name
  42 + self.name = "csiactcopy"
  43 + self.version = "1.1.0"
  44 + self.datapath = os.getenv("VHEFITS","")
  45 +
  46 + # Make sure that parfile exists
  47 + file = self.parfile()
  48 +
  49 + # Initialise application
  50 + if len(argv) == 0:
  51 + ctools.cscript.__init__(self, self.name, self.version)
  52 + elif len(argv) == 1:
  53 + ctools.cscript.__init__(self, self.name, self.version, *argv)
  54 + else:
  55 + raise TypeError("Invalid number of arguments given.")
  56 +
  57 + # Set logger properties
  58 + self.log_header()
  59 + self.log.date(True)
  60 +
  61 + # Return
  62 + return
  63 +
  64 + def __del__(self):
  65 + """
  66 + Destructor.
  67 + """
  68 +
  69 + # Return
  70 + return
  71 +
  72 + def parfile(self):
  73 + """
  74 + Check if parfile exists. If parfile does not exist then create a
  75 + default parfile. This kluge avoids shipping the cscript with a
  76 + parfile.
  77 + """
  78 + # Set parfile name
  79 + parfile = self.name+".par"
  80 +
  81 + try:
  82 + pars = gammalib.GApplicationPars(parfile)
  83 + except:
  84 + # Signal if parfile was not found
  85 + sys.stdout.write("Parfile "+parfile+" not found. Create default parfile.\n")
  86 +
  87 + # Create default parfile
  88 + pars = gammalib.GApplicationPars()
  89 + pars.append(gammalib.GApplicationPar("remote_master","f","a","/Volumes/mountpoint/master.json","","","Location of remote master file"))
  90 + pars.append(gammalib.GApplicationPar("prodname","s","a","iact-fits","","","Name of FITS production to download"))
  91 + pars.append(gammalib.GApplicationPar("outpath","s","a","/path/to/fits","","","Destination path of FITS data"))
  92 + pars.append(gammalib.GApplicationPar("runlist","f","h","NONE","","","List of observation IDs"))
  93 + pars.append_standard()
  94 + pars.append(gammalib.GApplicationPar("logfile","f","h","csiactcopy.log","","","Log filename"))
  95 + pars.save(parfile)
  96 +
  97 + # Return
  98 + return
  99 +
  100 + def get_parameters(self):
  101 + """
  102 + Get parameters from parfile and setup the observation.
  103 + """
  104 +
  105 + # Get Parameters
  106 + self.m_remote_master = gammalib.expand_env(self["remote_master"].filename())
  107 + if not os.path.isfile(self.m_remote_master):
  108 + raise RuntimeError("*** ERROR: FITS data not available. No master index file found in \""+self.m_remote_master+"\". Make sure remote file system is properly mounted")
  109 +
  110 + # Get parameters
  111 + self.m_prodname = self["prodname"].string()
  112 + self.m_outpath = gammalib.expand_env(self["outpath"].string())
  113 + self.m_runlist = self["runlist"].filename()
  114 +
  115 + # Return
  116 + return
  117 +
  118 + def execute(self):
  119 + """
  120 + Execute the script.
  121 + """
  122 + # Run the script
  123 + self.run()
  124 +
  125 + # Return
  126 + return
  127 +
  128 + def run(self):
  129 + """
  130 + Run the script.
  131 + """
  132 + # Switch screen logging on
  133 + self.log.cout(True)
  134 +
  135 + # Get parameters
  136 + self.get_parameters()
  137 +
  138 + # Write input parameters into logger
  139 + if self.logTerse():
  140 + self.log_parameters()
  141 + self.log("\n")
  142 +
  143 + # Make destination directory if not available
  144 + if not os.path.isdir(self.m_outpath):
  145 + os.makedirs(self.m_outpath)
  146 +
  147 + # Log output
  148 + if self.logTerse():
  149 + self.log("\n")
  150 + self.log.header2("Runlist information")
  151 +
  152 + # Initialise runlist array
  153 + self.runlist = []
  154 +
  155 + # Check if runlist file is available
  156 + if os.path.isfile(self.m_runlist):
  157 + runfile = open(self.m_runlist)
  158 + for line in runfile.readlines():
  159 + if len(line) == 0:
  160 + continue
  161 + if line[0] == '#':
  162 + continue
  163 + if len(line.split()) > 0:
  164 + self.runlist.append(int(line.split()[0]))
  165 + runfile.close()
  166 +
  167 + # Logging
  168 + if self.logTerse():
  169 + self.log.parformat("Number of observations")
  170 + self.log(str(len(self.runlist)))
  171 + self.log("\n")
  172 +
  173 + # Copy all data if runlist file is "NONE"
  174 + elif self.m_runlist == "NONE":
  175 + if self.logTerse():
  176 + self.log("Copy all available data")
  177 + self.log("\n")
  178 +
  179 + # Raise exception if file not valid
  180 + else:
  181 + raise RuntimeError("*** ERROR: Runlist file \""+self.m_runlist+"\" not available")
  182 +
  183 + # Check for availability of remote master file
  184 + if not os.path.isfile(self.m_remote_master):
  185 + raise RuntimeError("*** ERROR: Remote master file \""+self.m_remote_master+"\" does not exist")
  186 +
  187 + # Retrieve json data from remote master
  188 + json_data = open(self.m_remote_master).read()
  189 + data = json.loads(json_data)
  190 + if not "datasets" in data:
  191 + raise RuntimeError("*** ERROR: Key \"datasets\" not available in remote master index file \""+self.m_remote_master+"\".")
  192 +
  193 + # Get array of configurations
  194 + configs = data["datasets"]
  195 +
  196 + # Get remote paths
  197 + self.remote_base = os.path.dirname(self.m_remote_master)
  198 +
  199 + # Initialise flag if prod has been found
  200 + has_prod = False
  201 +
  202 + # Initialise file names to be copied
  203 + files = set()
  204 +
  205 + # Logging
  206 + if self.logTerse():
  207 + self.log("\n")
  208 + self.log.header2("Loop over remote configs")
  209 +
  210 + # Loop over configs
  211 + for config in configs:
  212 +
  213 + # Logging
  214 + if self.logVerbose():
  215 + self.log.header2(str(config["name"]))
  216 +
  217 + # Check if prodname was found
  218 + if config["name"] == self.m_prodname:
  219 +
  220 + # Inidicate that prodname has been found
  221 + has_prod = True
  222 +
  223 + # Build path of index files
  224 + remote_hdu = os.path.join(self.remote_base, config["hduindx"])
  225 + remote_obs = os.path.join(self.remote_base, config["obsindx"])
  226 +
  227 + # Log information
  228 + if self.logTerse():
  229 + self.log.header3("Remote config \""+self.m_prodname+"\"")
  230 + self.log.parformat("HDU index")
  231 + self.log(str(remote_hdu))
  232 + self.log("\n")
  233 + self.log.parformat("Observation index")
  234 + self.log(str(remote_obs))
  235 + self.log("\n")
  236 +
  237 + # Open remote HDU index file
  238 + fits = gammalib.GFits(str(remote_hdu))
  239 + table = fits["HDU_INDEX"]
  240 +
  241 + # Initialise flag if SIZE column is present
  242 + has_size = table.contains("SIZE")
  243 +
  244 + if has_size:
  245 + # Initialise file size
  246 + cp_size = 0
  247 +
  248 + # Initialise remote observation IDs
  249 + remote_ids = set()
  250 + for row in range(table.nrows()):
  251 + remote_ids.add(table["OBS_ID"][row])
  252 +
  253 + # Log runs that are not available remotely
  254 + for run in self.runlist:
  255 + if not run in remote_ids:
  256 + if self.logNormal():
  257 + self.log("Skip observation "+str(run)+": ID not available remotely")
  258 + self.log("\n")
  259 +
  260 +
  261 + # Loop over remote HDU index file
  262 + for row in range(table.nrows()):
  263 +
  264 + # Get observation ID
  265 + obs_id = table["OBS_ID"][row]
  266 + file_dir = table["FILE_DIR"][row]
  267 + file_name = table["FILE_NAME"][row]
  268 +
  269 + # Skip if filename is empty
  270 + if file_name == "":
  271 + continue
  272 +
  273 + # Check if we need to consider an input runlist
  274 + if len(self.runlist):
  275 +
  276 + # Check if obs id is in runlist
  277 + if obs_id in self.runlist:
  278 +
  279 + # Get filename
  280 + fname = os.path.join(os.path.dirname(remote_hdu), file_dir, file_name)
  281 +
  282 + # Add file
  283 + oldlen = len(files)
  284 + files.add(fname)
  285 + newlen = len(files)
  286 +
  287 + # Add file size
  288 + if has_size and newlen > oldlen:
  289 + cp_size += table["SIZE"][row]
  290 +
  291 + # Otherwise add every file
  292 + else:
  293 + # Get filename
  294 + fname = os.path.join(os.path.dirname(remote_hdu), file_dir, file_name)
  295 +
  296 + # Add file
  297 + oldlen = len(files)
  298 + files.add(fname)
  299 + newlen = len(files)
  300 +
  301 + # Add file size
  302 + if has_size and newlen > oldlen:
  303 + cp_size += table["SIZE"][row]
  304 +
  305 + # Log file information
  306 + if self.logTerse():
  307 + self.log("\n")
  308 + self.log.header2("File information")
  309 + self.log.parformat("Number of files")
  310 + self.log(str(len(files)))
  311 + self.log("\n")
  312 + if has_size:
  313 + self.log.parformat("Size")
  314 + self.log("%.2f"%(float(cp_size)*1e-6)+" MB")
  315 + self.log("\n")
  316 + if self.logVerbose():
  317 + self.log("\n")
  318 + self.log.header3("File names")
  319 + for filename in files:
  320 + self.log(str(filename)+"\n")
  321 +
  322 + # Close HDU index file
  323 + fits.close()
  324 +
  325 + # If prodname is not found just log that we skip the config
  326 + else:
  327 + if self.logExplicit():
  328 + self.log("\n")
  329 + self.log.header3("Skipping config \""+str(config["name"])+"\"")
  330 + self.log("\n")
  331 +
  332 + # Raise Exception if prodname was not found
  333 + if not has_prod:
  334 + msg = "*** ERROR: FITS production \""+self.m_prodname+"\" not available. "
  335 + msg += "Available productions are: \n"
  336 + for config in configs:
  337 + msg += " - "+config["name"] + "\n"
  338 + raise RuntimeError(msg)
  339 +
  340 + # Logging
  341 + if self.logNormal():
  342 + self.log("\n")
  343 + self.log.header1("Copying files")
  344 +
  345 + # intialise counter
  346 + k = 0
  347 +
  348 + # initialise values for logging
  349 + last_fraction = 0.0
  350 + fraction_increment = 20.0
  351 + if self.logNormal():
  352 + fraction_increment = 10.0
  353 + if self.logTerse():
  354 + fraction_increment = 5.0
  355 + if self.logExplicit():
  356 + fraction_increment = 2.0
  357 +
  358 + # initialise logging properties
  359 + n_copied = 0
  360 + total_size = 0.0
  361 +
  362 + # Loop over files and copy
  363 + for filename in files:
  364 +
  365 + # Log progress
  366 + fraction = float(k) / float(len(files)) * 100.0
  367 + while fraction > last_fraction:
  368 + if self.logNormal() and not self.logVerbose():
  369 + self.log.parformat("Status")
  370 + self.log(str(int(last_fraction))+"%")
  371 + self.log("\n")
  372 + last_fraction += fraction_increment
  373 +
  374 + # Copy file
  375 + filesize = self.copy(filename, self.clobber())
  376 +
  377 + if filesize > 0.0:
  378 + total_size += filesize
  379 + n_copied += 1
  380 +
  381 + # Increment counter
  382 + k += 1
  383 +
  384 + # Logging
  385 + if self.logNormal() and not self.logVerbose():
  386 + self.log.parformat("Status")
  387 + self.log("Finished")
  388 + self.log("\n")
  389 + if self.logTerse():
  390 + self.log("\n")
  391 + self.log.header1("Updating index files")
  392 +
  393 + # Build local index file names
  394 + local_hdu = os.path.join(self.m_outpath, os.path.relpath(remote_hdu, self.remote_base))
  395 + local_obs = os.path.join(self.m_outpath, os.path.relpath(remote_obs, self.remote_base))
  396 +
  397 + # If we have a runlist then merge index file
  398 + if len(self.runlist):
  399 +
  400 + # Logging
  401 + if self.logTerse():
  402 + self.log("\n")
  403 + self.log.header3("HDU index")
  404 +
  405 + # Merge remote index files with local files
  406 + self.merge(local_hdu, remote_hdu, "HDU_INDEX", self.clobber())
  407 +
  408 + if self.logTerse():
  409 + self.log("\n")
  410 + self.log.header3("OBS index")
  411 +
  412 + # Merge remote index files with local files
  413 + self.merge(local_obs, remote_obs, "OBS_INDEX", self.clobber())
  414 +
  415 + else:
  416 + # If all files were copied, just copy index files too
  417 + self.copy(remote_hdu, self.clobber())
  418 + self.copy(remote_obs, self.clobber())
  419 +
  420 + if self.logTerse():
  421 + self.log("\n")
  422 + self.log.header3("Master index file")
  423 +
  424 + # Adding prodname to local master
  425 + localmaster = os.path.join(self.m_outpath, "master.json")
  426 +
  427 + # If no local master is found, copy master over first
  428 + if not os.path.isfile(localmaster):
  429 + self.copy(self.m_remote_master, self.clobber())
  430 +
  431 + # Load local master
  432 + json_data = open(localmaster).read()
  433 + data = json.loads(json_data)
  434 + configs = data["datasets"]
  435 +
  436 + # Initialise flag indicating if we already have prodname in master file
  437 + has_config = False
  438 +
  439 + # Initialise new configs array
  440 + newconfigs = []
  441 +
  442 + # Loop over configs in master index file
  443 + for config in configs:
  444 +
  445 + # Get hdu and obs index files
  446 + hdu = os.path.join(self.m_outpath, config["hduindx"])
  447 + obs = os.path.join(self.m_outpath, config["obsindx"])
  448 +
  449 + # Check if index files are available
  450 + if not (gammalib.is_fits(str(hdu)) and gammalib.is_fits(str(obs))):
  451 + if self.logTerse():
  452 + self.log("Removing \""+str(config["name"])+"\" (not available)")
  453 + self.log("\n")
  454 + else:
  455 + # Append config if valid
  456 + newconfigs.append(config)
  457 + if self.logTerse():
  458 + self.log("Keeping \""+str(config["name"])+"\"")
  459 + self.log("\n")
  460 +
  461 + # Signals that downloaded config is available
  462 + if config["name"] == self.m_prodname:
  463 + has_config = True
  464 +
  465 + # Create new entry if config was not available
  466 + if not has_config:
  467 + newdict = dict.fromkeys(["name","hduindx","obsindx"])
  468 + newdict["name"] = self.m_prodname
  469 + newdict["hduindx"] = os.path.relpath(local_hdu, self.m_outpath)
  470 + newdict["obsindx"] = os.path.relpath(local_obs, self.m_outpath)
  471 + newconfigs.append(newdict)
  472 + if self.logTerse():
  473 + self.log("Adding \""+str(newdict["name"])+"\"")
  474 + self.log("\n")
  475 +
  476 + # Write new json master file
  477 + f = open(localmaster, "w")
  478 + data["datasets"] = newconfigs
  479 + json.dump(data, f, indent=2)
  480 + f.close()
  481 +
  482 + # Log summary
  483 + if self.logNormal():
  484 + self.log("\n")
  485 + self.log.header1("Summary")
  486 + self.log.parformat("Data files found")
  487 + self.log(str(len(files)))
  488 + self.log("\n")
  489 + self.log.parformat("Data files copied")
  490 + self.log(str(n_copied))
  491 + self.log("\n")
  492 + self.log.parformat("Size")
  493 + self.log("%.2f"%(float(total_size)*1e-6)+" MB")
  494 + self.log("\n")
  495 + self.log.parformat("Local configs")
  496 + self.log(str(len(newconfigs)))
  497 + self.log("\n")
  498 + if self.logTerse():
  499 + self.log("\n")
  500 + self.log.header3("Content of master index")
  501 + for config in newconfigs:
  502 + self.log(str(config["name"]))
  503 + self.log("\n")
  504 +
  505 + # Return
  506 + return
  507 +
  508 + def copy(self, source, clobber):
  509 + """
  510 + copy file to m_outpath
  511 + """
  512 + # Get file destination
  513 + destination = os.path.join(self.m_outpath,os.path.relpath(source, self.remote_base))
  514 +
  515 + # Initialise return value
  516 + filesize = 0.0
  517 +
  518 + # Logging
  519 + if self.logVerbose():
  520 + self.log("\n")
  521 + self.log.header3("Copy file")
  522 + self.log.parformat("Source")
  523 + self.log(str(source))
  524 + self.log("\n")
  525 + self.log.parformat("Destination")
  526 + self.log(str(destination))
  527 + self.log("\n")
  528 + self.log.parformat("Already exists")
  529 + self.log(str(os.path.isfile(destination)))
  530 + self.log("\n")
  531 + self.log.parformat("Overwrite")
  532 + self.log(str(clobber))
  533 + self.log("\n")
  534 + self.log.parformat("Copying")
  535 +
  536 + # Flag if file destination is already available
  537 + is_file = os.path.isfile(destination)
  538 +
  539 + # check if file could be skipped because clobber=no
  540 + if is_file and clobber == False:
  541 + if self.logVerbose():
  542 + self.log("Skip (clobber=no)")
  543 + self.log("\n")
  544 +
  545 + # check if file could be skipped because it is the same file
  546 + elif is_file and os.path.samefile(destination, source):
  547 + if self.logVerbose():
  548 + self.log("Skip (same file)")
  549 + self.log("\n")
  550 +
  551 + else:
  552 + # Create directory if not existent
  553 + dest_dir = os.path.dirname(destination)
  554 + if not os.path.isdir(dest_dir):
  555 + os.makedirs(dest_dir)
  556 +
  557 + # Copy file (if source not identical to destination)
  558 + shutil.copy2(source, destination)
  559 +
  560 + # Get filesize
  561 + filesize = os.stat(destination).st_size
  562 +
  563 + # Logging
  564 + if self.logVerbose():
  565 + self.log("Done!")
  566 + self.log("\n")
  567 +
  568 + # Return
  569 + return filesize
  570 +
  571 +
  572 + def merge(self, localfits, remotefits, hduname, clobber):
  573 + """
  574 + merge remote and local fits file
  575 + If local fits file not present, a new one is created
  576 + """
  577 +
  578 + if self.logTerse():
  579 + self.log.parformat("Local file")
  580 + self.log(str(localfits))
  581 + self.log("\n")
  582 + self.log.parformat("Remote file")
  583 + self.log(str(remotefits))
  584 + self.log("\n")
  585 + self.log.parformat("HDU name")
  586 + self.log(str(hduname))
  587 + self.log("\n")
  588 + self.log.parformat("Conflict preference")
  589 + if clobber:
  590 + self.log("Remote")
  591 + else:
  592 + self.log("Local")
  593 + self.log("\n")
  594 +
  595 + # Check if local fits file is available
  596 + if os.path.isfile(localfits):
  597 + local = gammalib.GFits(str(localfits))
  598 + local_hdu = local[hduname]
  599 + else:
  600 + # Otherwise load remote fits file and delete rows
  601 + local = gammalib.GFits(str(remotefits))
  602 + local_hdu = local[hduname]
  603 + local_hdu.remove_rows(0,local_hdu.nrows())
  604 +
  605 + # find local obs_id
  606 + local_obs = []
  607 + lobs_col = local_hdu["OBS_ID"]
  608 + for obs_id in lobs_col:
  609 + local_obs.append(obs_id)
  610 +
  611 + # Create selection expression for opening remote fits file
  612 + selection = ""
  613 + if len(self.runlist):
  614 + selection += "["+hduname+"]["
  615 + for run in self.runlist:
  616 + selection += "OBS_ID=="+str(run)
  617 + selection += "||"
  618 + selection = selection[:-2]
  619 + selection += "]"
  620 +
  621 + # Create file name
  622 + remotefile = remotefits + selection
  623 +
  624 + # open remote fits file
  625 + remote = gammalib.GFits(str(remotefile))
  626 + remote_hdu = remote[hduname]
  627 +
  628 + # Get remote obs_id
  629 + remote_obs = []
  630 + robs_col = remote_hdu["OBS_ID"]
  631 + for obs_id in robs_col:
  632 + remote_obs.append(obs_id)
  633 +
  634 + if self.logTerse():
  635 + self.log.parformat("Remote entries")
  636 + self.log(str(len(remote_obs)))
  637 + self.log("\n")
  638 + self.log.parformat("Local entries")
  639 + self.log(str(len(local_obs)))
  640 + self.log("\n")
  641 +
  642 + # initialise counter for logging
  643 + removed_rows = 0
  644 +
  645 + # If clobber=yes, we use the remote content to overwrite corresponding local content
  646 + if clobber:
  647 +
  648 + # Loop over remote obs_ids
  649 + for remote_obs_id in remote_obs:
  650 +
  651 + # Check if remote obs_id is present locally
  652 + if remote_obs_id in local_obs:
  653 +
  654 + # Remove local obs_id entries
  655 + table_has_obsid = True
  656 + while table_has_obsid:
  657 + for i in range(local_hdu.nrows()):
  658 + if remote_obs_id == local_hdu["OBS_ID"][i]:
  659 + local_hdu.remove_rows(i,1)
  660 + removed_rows += 1
  661 + break
  662 + table_has_obsid = False
  663 + for i in range(local_hdu.nrows()):
  664 + if remote_obs_id == local_hdu["OBS_ID"][i]:
  665 + table_has_obsid = True
  666 + break
  667 +
  668 + # If clobber=false, we keep the local content unchanged and just expand by remote content
  669 + else:
  670 + # Loop over remote obs_ids
  671 + for local_obs_id in local_obs:
  672 +
  673 + # Check if local obs_id is present remotely
  674 + if local_obs_id in remote_obs:
  675 +
  676 + # Remove remote obs_id entries
  677 + table_has_obsid = True
  678 + while table_has_obsid:
  679 + for i in range(remote_hdu.nrows()):
  680 + if local_obs_id == remote_hdu["OBS_ID"][i]:
  681 + remote_hdu.remove_rows(i,1)
  682 + removed_rows += 1
  683 + break
  684 + table_has_obsid = False
  685 + for i in range(remote_hdu.nrows()):
  686 + if local_obs_id == remote_hdu["OBS_ID"][i]:
  687 + table_has_obsid = True
  688 + break
  689 +
  690 + # Store number of local rows
  691 + old_local_rows = local_hdu.nrows()
  692 +
  693 + # Add rmeote HDUs
  694 + local_hdu.insert_rows(old_local_rows, remote_hdu.nrows())
  695 +
  696 + if self.logTerse():
  697 + if clobber:
  698 + self.log.parformat("Removed local rows")
  699 + else:
  700 + self.log.parformat("Skipped remote rows")
  701 + self.log(str(removed_rows))
  702 + self.log("\n")
  703 +
  704 + # Loop over columns
  705 + for i in range(local_hdu.ncols()):
  706 +
  707 + # Get local and remote columns
  708 + local_col = local_hdu[i]
  709 + remote_col = remote_hdu[i]
  710 +
  711 + # Loop over entries and merge
  712 + for j in range(remote_col.length()):
  713 + local_col[j+old_local_rows] = remote_col[j]
  714 +
  715 + # Save local fits file
  716 + local.saveto(str(localfits), True)
  717 +
  718 + # Return
  719 + return
  720 +
  721 +# ======================== #
  722 +# Main routine entry point #
  723 +# ======================== #
  724 +if __name__ == '__main__':
  725 + """
  726 + Copy IACT data
  727 + """
  728 + # Create instance of application
  729 + app = csiactcopy(sys.argv)
  730 +
  731 + # Open logfile
  732 + app.logFileOpen()
  733 +
  734 + # Execute application
  735 + app.execute()
cscripts/csiactobs.py
@@ -47,9 +47,10 @@ class csiactobs(ctools.cscript): @@ -47,9 +47,10 @@ class csiactobs(ctools.cscript):
47 47
48 # Initialise some members 48 # Initialise some members
49 self.m_ebounds = gammalib.GEbounds() 49 self.m_ebounds = gammalib.GEbounds()
50 - self.datapath = os.getenv("VHEFITS","")  
51 - self.inmodels = None  
52 - self.outobs = "obs.xml" 50 + self.datapath = os.getenv("VHEFITS","")
  51 + self.inmodels = None
  52 + self.xml = gammalib.GXml()
  53 + self.models = gammalib.GModels()
53 54
54 # Make sure that parfile exists 55 # Make sure that parfile exists
55 file = self.parfile() 56 file = self.parfile()
@@ -141,10 +142,10 @@ class csiactobs(ctools.cscript): @@ -141,10 +142,10 @@ class csiactobs(ctools.cscript):
141 self.m_bkgpars = self["bkgpars"].integer() 142 self.m_bkgpars = self["bkgpars"].integer()
142 143
143 # Output model file 144 # Output model file
144 - self.outmodel = self["outmodel"].filename() 145 + self.m_outmodel = self["outmodel"].filename()
145 146
146 # Observation outfile 147 # Observation outfile
147 - self.outobs = self["outobs"].filename() 148 + self.m_outobs = self["outobs"].filename()
148 149
149 # Master index file name 150 # Master index file name
150 self.m_master_indx = self["master_indx"].string() 151 self.m_master_indx = self["master_indx"].string()
@@ -332,6 +333,24 @@ class csiactobs(ctools.cscript): @@ -332,6 +333,24 @@ class csiactobs(ctools.cscript):
332 Returns runlist energy range 333 Returns runlist energy range
333 """ 334 """
334 return self.m_ebounds 335 return self.m_ebounds
  336 +
  337 + def obs(self):
  338 + """
  339 + Returns GObservations object
  340 + """
  341 + # Initialise observations
  342 + obs = gammalib.GObservations()
  343 +
  344 + # Read observations if XML is filled
  345 + if self.xml.size():
  346 + obs.read(self.xml)
  347 +
  348 + # Assign models
  349 + obs.models(self.models)
  350 +
  351 + # Return observations
  352 + return obs
  353 +
335 354
336 def execute(self): 355 def execute(self):
337 """ 356 """
@@ -341,7 +360,7 @@ class csiactobs(ctools.cscript): @@ -341,7 +360,7 @@ class csiactobs(ctools.cscript):
341 self.run() 360 self.run()
342 361
343 # Save residual map 362 # Save residual map
344 - self.save(self.outobs, self.m_clobber) 363 + self.save(self.m_outobs, self.m_clobber)
345 364
346 # Return 365 # Return
347 return 366 return
@@ -362,11 +381,11 @@ class csiactobs(ctools.cscript): @@ -362,11 +381,11 @@ class csiactobs(ctools.cscript):
362 self.log_parameters() 381 self.log_parameters()
363 self.log("\n") 382 self.log("\n")
364 383
365 - # Initialise empty models  
366 - self.models = gammalib.GModels() 384 + # Clear models
  385 + self.models.clear()
367 386
368 - # Initialise empty xml file and append an observation list  
369 - self.xml = gammalib.GXml() 387 + # Clear xml file and append an observation list
  388 + self.xml.clear()
370 self.xml.append(gammalib.GXmlElement("observation_list title=\"observation list\"")) 389 self.xml.append(gammalib.GXmlElement("observation_list title=\"observation list\""))
371 lib = self.xml.element("observation_list", 0) 390 lib = self.xml.element("observation_list", 0)
372 391
@@ -476,7 +495,7 @@ class csiactobs(ctools.cscript): @@ -476,7 +495,7 @@ class csiactobs(ctools.cscript):
476 495
477 # Handle background 496 # Handle background
478 index = -1 497 index = -1
479 - bkg_mod_hierarchy = self.m_bkg_mod_hiera 498 + bkg_mod_hierarchy = list(self.m_bkg_mod_hiera)
480 for version in self.m_bkg_hiera: 499 for version in self.m_bkg_hiera:
481 n = formats.count(version) 500 n = formats.count(version)
482 if n > 0: 501 if n > 0:
@@ -489,7 +508,8 @@ class csiactobs(ctools.cscript): @@ -489,7 +508,8 @@ class csiactobs(ctools.cscript):
489 if self.logTerse(): 508 if self.logTerse():
490 self.log("Warning: observation "+str(obs_id)+" has no background information (file=\""+bkgfile+"\"). IRF background cannot be used\n") 509 self.log("Warning: observation "+str(obs_id)+" has no background information (file=\""+bkgfile+"\"). IRF background cannot be used\n")
491 bkgfile = "" 510 bkgfile = ""
492 - bkg_mod_hierarchy.remove("irf") 511 + if "irf" in bkg_mod_hierarchy:
  512 + bkg_mod_hierarchy.remove("irf")
493 513
494 # Close hdu index file 514 # Close hdu index file
495 hduindx.close() 515 hduindx.close()
@@ -627,7 +647,7 @@ class csiactobs(ctools.cscript): @@ -627,7 +647,7 @@ class csiactobs(ctools.cscript):
627 self.xml.save(outfile) 647 self.xml.save(outfile)
628 648
629 # Save model XML file 649 # Save model XML file
630 - self.models.save(self.outmodel) 650 + self.models.save(self.m_outmodel)
631 651
632 # Return 652 # Return
633 return 653 return
doc/source/reference_manual/csiactcopy.rst 0 → 100644
  1 +.. _csiactcopy:
  2 +
  3 +csiactcopy
  4 +===========
  5 +
  6 +Copies IACT data from one location to another
  7 +
  8 +
  9 +Synopsis
  10 +--------
  11 +
  12 +This script allows to download IACT data from a remote machine. The remote file system
  13 +has to be mounted before running the script. For mounting, e.g. sshfs can be used:
  14 +
  15 +.. code-block:: bash
  16 +
  17 + $ sshfs user@remote.server.com:/path/to/remote/fits /path/to/mountpoint/
  18 +
  19 +The script can optionally take an ASCII file with a list of observation IDs as input.
  20 +In this way the user can copy only a specific subset of the data. Index files and data
  21 +structure is updated accordingly by the script.
  22 +
  23 +In case the copying procedure failed due to e.g. a broken connection, the script can be executed
  24 +again using the parameter ``clobber=no``. Thus files that were already copied don't get
  25 +recopied and overwritten which saves time.
  26 +
  27 +In order to monitor the progress on the screen, the script can be executed with the
  28 +hidden parameter ``debug=yes``
  29 +
  30 +
  31 +General parameters
  32 +------------------
  33 +
  34 +``remote_master [file]``
  35 + Location of remote master file
  36 +
  37 +``prodname [string]``
  38 + Name of FITS production to download
  39 +
  40 +``outpath [file]``
  41 + Destination path of FITS data
  42 +
  43 +``(runlist = NONE) [file]``
  44 + List of observation IDs
  45 +
  46 +
  47 +Standard parameters
  48 +-------------------
  49 +
  50 +``(chatter = 2) [integer]``
  51 + Verbosity of the executable:
  52 + chatter = 0: no information will be logged
  53 +
  54 + chatter = 1: only errors will be logged
  55 +
  56 + chatter = 2: errors and actions will be logged
  57 +
  58 + chatter = 3: report about the task execution
  59 +
  60 + chatter = 4: detailed report about the task execution
  61 +
  62 +``(clobber = yes) [boolean]``
  63 + Specifies whether an existing output counts cube should be overwritten.
  64 +
  65 +``(debug = no) [boolean]``
  66 + Enables debug mode. In debug mode the executable will dump any log file output to the console.
  67 +
  68 +``(mode = ql) [string]``
  69 + Mode of automatic parameters (default is "ql", i.e. "query and learn").
  70 +
  71 +``(logfile = csiactdload.log) [filename]``
  72 + Log filename.
  73 +
  74 +
  75 +Related tools or scripts
  76 +------------------------
  77 +
  78 +:doc:`csiactdata`
0 \ No newline at end of file 79 \ No newline at end of file
doc/source/reference_manual/index.rst
@@ -61,3 +61,4 @@ IACT cscripts @@ -61,3 +61,4 @@ IACT cscripts
61 csiactdata --- Shows information about IACT data available on the user machine <csiactdata> 61 csiactdata --- Shows information about IACT data available on the user machine <csiactdata>
62 csiactobs --- Generates observation definition file for IACT data from observation IDs <csiactobs> 62 csiactobs --- Generates observation definition file for IACT data from observation IDs <csiactobs>
63 csfindobs --- Generates a list of IACT observation IDs <csfindobs> 63 csfindobs --- Generates a list of IACT observation IDs <csfindobs>
  64 + csiactcopy --- Copies IACT data from one location to another <csiactcopy>
test/test_cscripts.sh.in
@@ -483,6 +483,26 @@ $ECHO &quot; ok&quot; @@ -483,6 +483,26 @@ $ECHO &quot; ok&quot;
483 483
484 484
485 # 485 #
  486 +# Test csiactcopy
  487 +# ===============
  488 +$ECHO -n "Test csiactcopy: "
  489 +chmod +x @abs_top_srcdir@/cscripts/csiactcopy.py
  490 +@abs_top_srcdir@/cscripts/csiactcopy.py \
  491 + remote_master="iactdata/master.json" \
  492 + outpath="iactdata_test" \
  493 + prodname="unit-test" > csiactcopy.log
  494 +$ECHO -n "."
  495 +if [ -s "iactdata_test/master.json" ]
  496 +then
  497 + $ECHO -n "."
  498 +else
  499 + $ECHO " iactdata_test/master.json file is not valid"
  500 + exit 1
  501 +fi
  502 +$ECHO " ok"
  503 +
  504 +
  505 +#
486 # Test csfindobs 506 # Test csfindobs
487 # ============== 507 # ==============
488 $ECHO -n "Test csfindobs: " 508 $ECHO -n "Test csfindobs: "