#!/usr/bin/env python
# ==========================================================================
# Copies IACT data from remote machine
#
# Copyright (C) 2016-2017 Michael Mayer
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# ==========================================================================
import os
import sys
import json
import shutil
import gammalib
import ctools
# ================ #
# csiactdata class #
# ================ #
class csiactcopy(ctools.cscript):
"""
Copies IACT data from remote machine
This script copies IACT data from one location to another. It can
take a list of observation IDs to allow the download specific
observations. Index files get merged and updated accordingly.
"""
# Constructor
def __init__(self, *argv):
"""
Constructor
"""
# Set name
self._name = 'csiactcopy'
self._version = ctools.__version__
self._datapath = os.getenv('VHEFITS','')
self._remote_master = gammalib.GFilename()
self._remote_base = ''
self._prodname = ''
self._outpath = ''
self._runlist = gammalib.GFilename()
self._runs = []
# Initialise application by calling the appropriate class
# constructor.
self._init_cscript(argv)
# Return
return
# Private methods
def _get_parameters(self):
"""
Get parameters from parfile and setup the observation
"""
# Get Parameters
self._remote_master = self['remote_master'].filename()
if not self._remote_master.exists():
raise RuntimeError('*** ERROR: FITS data not available. No '
'master index file found in "'+
self._remote_master+'". Make sure remote '
'file system is properly mounted.')
# Get parameters
self._prodname = self['prodname'].string()
self._outpath = gammalib.expand_env(self['outpath'].string())
self._runlist = self['runlist'].filename()
# Write input parameters into logger
self._log_parameters(gammalib.TERSE)
# Return
return
def _copy(self, source, clobber):
"""
Copy file to self._outpath directory
Parameters
----------
source : str
Path of file to be copied
Returns
-------
filesize : float
Size of the file that was copied
"""
# Get file destination
destination = os.path.join(self._outpath,
os.path.relpath(source, self._remote_base))
# Initialise return value
filesize = 0.0
# Flag if file destination is already available
is_file = os.path.isfile(destination)
# Logging
self._log_header3(gammalib.VERBOSE, 'Copy file')
self._log_value(gammalib.VERBOSE, 'Source', source)
self._log_value(gammalib.VERBOSE, 'Destination', destination)
self._log_value(gammalib.VERBOSE, 'Already exists', is_file)
self._log_value(gammalib.VERBOSE, 'Overwrite', clobber)
# Check if file could be skipped because clobber=no
if is_file and clobber == False:
self._log_value(gammalib.VERBOSE, 'Copying', 'Skip (clobber=no)')
# ... check if file could be skipped because it is the same file as
# inferred from the file path and name (the file is in fact not
# checked) ...
elif is_file and os.path.samefile(destination, source):
self._log_value(gammalib.VERBOSE, 'Copying', 'Skip (same file)')
# ... otherwise copy file
else:
# Create directory if it does not exist
dest_dir = os.path.dirname(destination)
if not os.path.isdir(dest_dir):
os.makedirs(dest_dir)
# If destination file exists then delete it. Make sure that file
# is writable for that
if os.path.isfile(destination):
os.chmod(destination, 420)
os.remove(destination)
# Copy file
shutil.copy2(source, destination)
# Get filesize
filesize = os.stat(destination).st_size
# Logging
self._log_value(gammalib.VERBOSE, 'Copying', 'Done!')
# Return
return filesize
def _merge(self, localfits, remotefits, hduname, clobber):
"""
Merge remote and local fits files
If the local fits file is not present, a new one is created.
Parameters
----------
localfits : str
Path of local index FITS file
remotefits : str
Path of remote index FITS file
hduname : str
Name of HDU extension to be merged
clobber : bool
Flag if remote content should overwrite local content
"""
# Write input parameters into logger
self._log_value(gammalib.NORMAL, 'Local file', localfits)
self._log_value(gammalib.NORMAL, 'Remote file', remotefits)
self._log_value(gammalib.NORMAL, 'HDU name', hduname)
if clobber:
preference = 'Remote'
else:
preference = 'Local'
self._log_value(gammalib.NORMAL, 'Conflict preference', preference)
# Check if local fits file is available
if os.path.isfile(localfits):
local = gammalib.GFits(str(localfits))
local_hdu = local[hduname]
else:
# Otherwise load remote fits file and delete rows
local = gammalib.GFits(str(remotefits))
local_hdu = local[hduname]
local_hdu.remove_rows(0,local_hdu.nrows())
# find local obs_id
local_obs = []
lobs_col = local_hdu['OBS_ID']
for obs_id in lobs_col:
local_obs.append(obs_id)
# Initialise run list. A kluge has been implemented to circumvent a
# too long file name: the run list is split into chunks of 50
# observations maximum. Note also the "//" division operator that
# makes sure that "i" is an integer on Python 3.
runlists = []
runs_per_chunk = 50
for i in range(len(self._runs) // runs_per_chunk + 1):
chunk = self._runs[i * runs_per_chunk : (i + 1) * runs_per_chunk]
runlists.append(chunk)
# Loop over runlist chunks
for i, runlist in enumerate(runlists):
# Create selection expression for opening remote fits file
selection = ''
if len(runlist):
selection += '['+hduname+']['
for run in runlist:
selection += 'OBS_ID=='+str(run)
selection += '||'
selection = selection[:-2]
selection += ']'
# Create file name
remotefile = remotefits + selection
# Open remote fits file
remote = gammalib.GFits(str(remotefile))
# If first iteration, create clone of FITS column for later use
if i == 0:
remote_hdu = remote[hduname].clone()
else:
# get temporary hdu to be added to current HDU
tmp_hdu = remote[hduname]
# Get old size of remote HDU
size = remote_hdu.nrows()
# Extend remote HDU
remote_hdu.append_rows(tmp_hdu.nrows())
# Loop over rows and columns to copy over values exteding remote HDU
for col in tmp_hdu:
for row in range(col.nrows()):
remote_hdu[col.name()][size + row] = col[row]
# Get remote obs_id
remote_obs = []
robs_col = remote_hdu['OBS_ID']
for obs_id in robs_col:
remote_obs.append(obs_id)
# Log entries of index files
self._log_value(gammalib.NORMAL, 'Remote entries', len(remote_obs))
self._log_value(gammalib.NORMAL, 'Local entries', len(local_obs))
# initialise counter for logging
removed_rows = 0
# If clobber=yes, we use the remote content to overwrite
# corresponding local content
if clobber:
# Loop over remote obs_ids
for remote_obs_id in remote_obs:
# Check if remote obs_id is present locally
if remote_obs_id in local_obs:
# Remove local obs_id entries
table_has_obsid = True
# Loop over obs ids and overwrite with remote content
while table_has_obsid:
# First remove old rows
for i in range(local_hdu.nrows()):
if remote_obs_id == local_hdu['OBS_ID'][i]:
local_hdu.remove_rows(i,1)
removed_rows += 1
break
# Replace with remote content
table_has_obsid = False
for i in range(local_hdu.nrows()):
if remote_obs_id == local_hdu['OBS_ID'][i]:
table_has_obsid = True
break
# If clobber=false, we keep the local content unchanged and just
# expand by remote content
else:
# Loop over remote obs_ids
for local_obs_id in local_obs:
# Check if local obs_id is present remotely
if local_obs_id in remote_obs:
# Remove remote obs_id entries
table_has_obsid = True
while table_has_obsid:
for i in range(remote_hdu.nrows()):
if local_obs_id == remote_hdu['OBS_ID'][i]:
remote_hdu.remove_rows(i,1)
removed_rows += 1
break
table_has_obsid = False
for i in range(remote_hdu.nrows()):
if local_obs_id == remote_hdu['OBS_ID'][i]:
table_has_obsid = True
break
# Store number of local rows
old_local_rows = local_hdu.nrows()
# Add remote HDUs
local_hdu.insert_rows(old_local_rows, remote_hdu.nrows())
# Log actions
if clobber:
what = 'Removed local rows'
else:
what = 'Skipped remote rows'
self._log_value(gammalib.NORMAL, what, removed_rows)
# Loop over columns
for i in range(local_hdu.ncols()):
# Get local and remote columns
local_col = local_hdu[i]
remote_col = remote_hdu[i]
# Loop over entries and merge
for j in range(remote_col.nrows()):
local_col[j+old_local_rows] = remote_col[j]
# Save local fits file
local.saveto(str(localfits), True)
# Return
return
def _set_runs(self, filename):
"""
Set the run list
Parameters
----------
filename : `~gammalib.GFilename`
Run list file name
Returns
-------
runs : list of int
Run list
"""
# Write header into logger
self._log_header1(gammalib.TERSE, 'Set runlist')
# Initialise runlist
runs = []
# If runlist file exists then open file and extract the run
# information into the run list
if filename.exists():
# Open the runlist file
runfile = open(filename.url())
# Read runlist file and append all run ID as integers to the
# run list. Skip any blank or comment lines
for line in runfile.readlines():
if len(line) == 0:
continue
if line[0] == '#':
continue
if len(line.split()) > 0:
run = int(line.split()[0])
runs.append(run)
self._log_value(gammalib.EXPLICIT, 'Run %d' % len(runs), run)
# Close runlist file
runfile.close()
# Logging
self._log_value(gammalib.NORMAL, 'Number of observations',
len(runs))
# ... otherwise, if the runlist file is 'NONE' then leave the
# run list empty. This implies copying all available data.
elif filename == 'NONE':
self._log_string(gammalib.NORMAL, 'Copy all available data')
# ... otherwise raise an exception
else:
msg = '*** ERROR: Runlist file "%s" not available' % filename
raise RuntimeError(msg)
# Return run list
return runs
# Public methods
def run(self):
"""
Run the script
"""
# Switch screen logging on in debug mode
if self._logDebug():
self._log.cout(True)
# Get parameters
self._get_parameters()
# Make destination directory if not available
if not os.path.isdir(self._outpath):
os.makedirs(self._outpath)
# Set run list
self._runs = self._set_runs(self._runlist)
# Check for availability of remote master file
if not self._remote_master.exists():
raise RuntimeError('*** ERROR: Remote master file "'+
self._remote_master+'" does not exist.')
# Retrieve json data from remote master
json_data = open(self._remote_master.url()).read()
data = json.loads(json_data)
if not 'datasets' in data:
raise RuntimeError('*** ERROR: Key "datasets" not available '
'in remote master index file "'+
self._remote_master+'".')
# Get array of configurations
configs = data['datasets']
# Get remote paths
self._remote_base = self._remote_master.path()
# Initialise flag if prod has been found
has_prod = False
# Initialise file names to be copied
files = set()
# Write header into logger
self._log_header2(gammalib.TERSE, 'Loop over remote configs')
# Loop over configs
for config in configs:
# Write header into logger. The str() is needed since
# "config['name']" is , and under Python 2 a
# string is expected.
self._log_header2(gammalib.VERBOSE, str(config['name']))
# Check if prodname was found
if config['name'] == self._prodname:
# Inidicate that prodname has been found
has_prod = True
# Build path of index files
remote_hdu = str(os.path.join(self._remote_base,
config['hduindx']))
remote_obs = str(os.path.join(self._remote_base,
config['obsindx']))
# Log information
self._log_header3(gammalib.NORMAL, 'Remote config "'+
self._prodname+'"')
self._log_value(gammalib.NORMAL, 'HDU index', remote_hdu)
self._log_value(gammalib.NORMAL, 'Observation index', remote_obs)
# Open remote HDU index file
fits = gammalib.GFits(str(remote_hdu))
table = fits['HDU_INDEX']
# Initialise flag if SIZE column is present
has_size = table.contains('SIZE')
# Initialise file size
if has_size:
cp_size = 0
# Initialise remote observation IDs
remote_ids = set()
for row in range(table.nrows()):
remote_ids.add(table['OBS_ID'][row])
# Log runs that are not available remotely
for run in self._runs:
# Check for run not in remote data store
if not run in remote_ids:
msg = ('Skip observation "%s": ID not available '
'remotely' % str(run))
self._log_string(msg)
# Loop over remote HDU index file
for row in range(table.nrows()):
# Get observation ID
obs_id = table['OBS_ID'][row]
file_dir = table['FILE_DIR'][row]
file_name = table['FILE_NAME'][row]
# Skip if filename is empty
if file_name == '':
continue
# Check if we need to consider an input runlist
if len(self._runs):
# Check if obs id is in runlist
if obs_id in self._runs:
# Get filename
fname = os.path.join(os.path.dirname(remote_hdu),
file_dir, file_name)
# Add file
oldlen = len(files)
files.add(fname)
newlen = len(files)
# Add file size
if has_size and newlen > oldlen:
cp_size += table['SIZE'][row]
# Otherwise add every file
else:
# Get filename
fname = os.path.join(os.path.dirname(remote_hdu),
file_dir, file_name)
# Add file
oldlen = len(files)
files.add(fname)
newlen = len(files)
# Add file size
if has_size and newlen > oldlen:
cp_size += table['SIZE'][row]
# Log file information
self._log_header2(gammalib.NORMAL, 'File information')
self._log_value(gammalib.NORMAL, 'Number of files', len(files))
if has_size:
size = float(cp_size) * 1.0e-6
self._log_value(gammalib.NORMAL, 'Size', '%.2f MB' % size)
self._log_header3(gammalib.VERBOSE, 'File names')
for filename in files:
self._log_string(gammalib.VERBOSE, str(filename)+'\n')
# Close HDU index file
fits.close()
# If prodname is not found just log that we skip the config
else:
self._log_header3(gammalib.EXPLICIT, 'Skipping config "'+
str(config['name'])+'"')
# Raise Exception if prodname was not found
if not has_prod:
msg = '*** ERROR: FITS production "'+self._prodname+'" not '
msg += 'available. Available productions are:\n'
for config in configs:
msg += ' - '+config['name']+'\n'
raise RuntimeError(msg)
# Write header
self._log_header1(gammalib.NORMAL, 'Copying files')
# Intialise counter
k = 0
# Initialise values for logging
last_fraction = 0.0
fraction_increment = 20.0
# Use 10% step increase
if self._logNormal():
fraction_increment = 10.0
# Use 5% step increase
if self._logTerse():
fraction_increment = 5.0
# Use 2% step increase
if self._logExplicit():
fraction_increment = 2.0
# Initialise logging properties
n_copied = 0
total_size = 0.0
# Loop over files and copy
for filename in files:
# Log progress
fraction = float(k) / float(len(files)) * 100.0
while fraction > last_fraction:
# Print status of copying procedure
self._log_value(gammalib.NORMAL, 'Status', '%d %%' %
int(last_fraction))
last_fraction += fraction_increment
# Copy file
filesize = self._copy(filename, self._clobber())
# If the filesize is positive then increment the number of copied
# files and add the size to the total copied filesize
if filesize > 0.0:
total_size += filesize
n_copied += 1
# Increment file counter
k += 1
# Logging
self._log_value(gammalib.NORMAL, 'Status', 'Finished')
# Logging about index files
self._log_header1(gammalib.TERSE, 'Updating index files')
# Build local hdu index file name
local_hdu = os.path.join(self._outpath,
os.path.relpath(remote_hdu,
self._remote_base))
# Build local obs index file name
local_obs = os.path.join(self._outpath,
os.path.relpath(remote_obs,
self._remote_base))
# If we have a runlist then merge index file
if len(self._runs):
# Logging
self._log_header3(gammalib.TERSE, 'HDU index')
# Merge remote index files with local files
self._merge(local_hdu, remote_hdu, 'HDU_INDEX', self._clobber())
# Logging
self._log_header3(gammalib.TERSE, 'OBS index')
# Merge remote index files with local files
self._merge(local_obs, remote_obs, 'OBS_INDEX', self._clobber())
else:
# If all files were copied, just copy index files too
self._copy(remote_hdu, self._clobber())
self._copy(remote_obs, self._clobber())
# Logging
self._log_header3(gammalib.TERSE, 'Master index file')
# Adding prodname to local master
localmaster = os.path.join(self._outpath, 'master.json')
# If no local master is found, copy master over first
if not os.path.isfile(localmaster):
self._copy(self._remote_master.url(), self._clobber())
# Load local master
json_data = open(localmaster).read()
data = json.loads(json_data)
configs = data['datasets']
# Initialise flag indicating if we already have prodname in master
# file
has_config = False
# Initialise new configs array
newconfigs = []
# Loop over configs in master index file
for config in configs:
# Get hdu and obs index files
hdu = os.path.join(self._outpath, config['hduindx'])
obs = os.path.join(self._outpath, config['obsindx'])
# Check if index files are available
if not (gammalib.GFilename(str(hdu)).is_fits() and
gammalib.GFilename(str(obs)).is_fits()):
self._log_value(gammalib.NORMAL,
'Removing "'+str(config['name']),
'Not available')
else:
# Append config if valid
newconfigs.append(config)
self._log_value(gammalib.NORMAL,
'Keeping "'+str(config['name']),
'Available')
# Signals that downloaded config is available
if config['name'] == self._prodname:
has_config = True
# Create new entry if config was not available
if not has_config:
newdict = dict.fromkeys(['name','hduindx','obsindx'])
newdict['name'] = self._prodname
newdict['hduindx'] = os.path.relpath(local_hdu, self._outpath)
newdict['obsindx'] = os.path.relpath(local_obs, self._outpath)
newconfigs.append(newdict)
if self._logTerse():
self._log('Adding "'+str(newdict['name'])+'"')
self._log('\n')
# Write new json master file. Make sure that we have write permission
# before writing the file. This is needed as the original master file
# may be read-only, and the shutil.copy2 function copies over the
# access permissions.
os.chmod(localmaster, 420)
f = open(localmaster, 'w')
data['datasets'] = newconfigs
json.dump(data, f, indent=2)
f.close()
# Compute total size in MB
size_MB = float(total_size) * 1.0e-6
# Log summary
self._log_header1(gammalib.NORMAL, 'Summary')
self._log_value(gammalib.NORMAL, 'Data files found', len(files))
self._log_value(gammalib.NORMAL, 'Data files copied', n_copied)
self._log_value(gammalib.NORMAL, 'Size', '%.2f MB' % size_MB)
self._log_value(gammalib.NORMAL, 'Local configs', len(newconfigs))
self._log_header3(gammalib.TERSE, 'Content of master index')
for config in newconfigs:
self._log_string(gammalib.TERSE, str(config['name'])+'\n')
# Return
return
def execute(self):
"""
Execute the script
"""
# Open logfile
self.logFileOpen()
# Read ahead output parameters
self._read_ahead(True)
# Run the script
self.run()
# Return
return
# ======================== #
# Main routine entry point #
# ======================== #
if __name__ == '__main__':
# Create instance of application
app = csiactcopy(sys.argv)
# Execute application
app.execute()