diff --git a/cscripts/Makefile.am b/cscripts/Makefile.am index 90a33ef..efd24db 100644 --- a/cscripts/Makefile.am +++ b/cscripts/Makefile.am @@ -5,14 +5,14 @@ pythonscripts = __init__.py \ cspull.py cssens.py cstsdist.py cscaldb.py csresmap.py \ csiactobs.py csiactdata.py csobsdef.py csspec.py cslightcrv.py csobsinfo.py \ csinfo.py csmodelinfo.py csmodelmerge.py cstsmapmerge.py \ - csobs2caldb.py \ + csobs2caldb.py csfindobs.py \ obsutils.py # Scripts for which symbolic links will be created in bindir linkscripts = cspull cssens cstsdist cscaldb csresmap \ csiactobs csiactdata csobsdef csspec cslightcrv csobsinfo \ csinfo csmodelinfo csmodelmerge cstsmapmerge \ - csobs2caldb + csobs2caldb csfindobs # Distribute scripts EXTRA_DIST = $(binscripts) $(pythonscripts) diff --git a/cscripts/csfindobs.py b/cscripts/csfindobs.py new file mode 100755 index 0000000..3a2da42 --- /dev/null +++ b/cscripts/csfindobs.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python +import gammalib +import ctools +import sys +import os +import json + +# ============== # +# csrunfinder class # +# ============== # +class csfindobs(ctools.cscript): + """ + This class can be used to find observations from an IACT data store. It derives + from the GammaLib::GApplication class which provides support for parameter + files, command line arguments, and logging. In that way the Python + script behaves just as a regular ctool. + """ + def __init__(self, *argv): + """ + Constructor. + """ + # Set name + self.name = "csfindobs" + self.version = "1.0.0" + self.verbose = False + self.datapath = os.getenv("VHEFITS","") + + # Make sure that parfile exists + file = self.parfile() + + # Initialise application + if len(argv) == 0: + ctools.cscript.__init__(self, self.name, self.version) + elif len(argv) ==1: + ctools.cscript.__init__(self, self.name, self.version, *argv) + else: + raise TypeError("Invalid number of arguments given.") + + # Set logger properties + self.log_header() + self.log.date(True) + + # Return + return + + def __del__(self): + """ + Destructor. + """ + # Write separator into logger + if self.logTerse(): + self.log("\n") + + # Return + return + + def parfile(self): + """ + Check if parfile exists. If parfile does not exist then create a + default parfile. This kluge avoids shipping the cscript with a parfile. + """ + # Set parfile name + parfile = self.name+".par" + + try: + pars = gammalib.GApplicationPars(parfile) + except: + # Signal if parfile was not found + sys.stdout.write("Parfile "+parfile+" not found. Create default parfile.\n") + + # Create default parfile + pars = gammalib.GApplicationPars() + pars.append(gammalib.GApplicationPar("datapath","s","a",self.datapath,"","","Path were data is located")) + pars.append(gammalib.GApplicationPar("prodname","s","a","prod-name","","","Name of FITS production (Run csiactdata to view your options)")) + pars.append(gammalib.GApplicationPar("ra","r","a","83.6331","","","Right ascension")) + pars.append(gammalib.GApplicationPar("dec","r","a","22.01","","","Declination")) + pars.append(gammalib.GApplicationPar("rad","r","a","2.5","","","Search radius")) + pars.append(gammalib.GApplicationPar("min_qual","i","h","0","0|1|2","","Minimum data quality (0=perfect, 1=ok, 2=bad)")) + pars.append(gammalib.GApplicationPar("expression","s","h","NONE","","","Additional expression")) + pars.append(gammalib.GApplicationPar("master_indx","s","h","master.json","","","Name of master index file")) + pars.append(gammalib.GApplicationPar("outfile","f","a","runlist.lis","","","Runlist outfile")) + pars.append_standard() + pars.save(parfile) + + # Return + return + + def get_parameters(self): + """ + Get parameters from parfile and setup the observation. + """ + # Get parameters + if self.datapath == "": + self.datapath = self["datapath"].string() + + # Expand environment + self.datapath = gammalib.expand_env(self.datapath) + + # Get production name + self.m_prodname = self["prodname"].string() + + # Master index file name + self.m_master_indx = self["master_indx"].string() + + # Intiialise flag if spatial selection is required + self.select_radec = True + + # Check for vailidity of spatial parameters + if self["ra"].is_valid() and self["dec"].is_valid() and self["rad"].is_valid(): + + # Read spatial parameters + self.m_ra = self["ra"].real() + self.m_dec = self["dec"].real() + self.m_radius = self["rad"].real() + + else: + self.select_radec = False + + # Check Radius for validity + if self.m_radius <= 0.0: + self.select_radec = False + + # Read other parameters + self.m_quality = self["min_qual"].integer() + self.m_expression = self["expression"].string() + + # Check for validity of expression + if self.m_expression == "NONE" or self.m_expression == "INDEF": + self.m_expression = "" + + # Read outfile + self.m_outfile = self["outfile"].filename() + + # Open master index file and look for prodname + master_file = os.path.join(self.datapath, self.m_master_indx) + if not os.path.isfile(master_file): + raise RuntimeError("FITS data store not available. No master index file found at \""+master_file+"\". Make sure the file is copied from the server and your datapath is set correctly.") + + # Open and load JSON file + json_data = open(master_file).read() + data = json.loads(json_data) + if not "datasets" in data: + raise RuntimeError("Key \"datasets\" not available in master index file.") + + # Get configurations + configs = data["datasets"] + + # Initialise obs index file + self.m_obs_index = "" + + # Get HDUs + for config in configs: + if self.m_prodname == config["name"]: + self.m_obs_index = str(os.path.join(self.datapath, config["obsindx"])) + break + + # Check HDUs + if self.m_obs_index == "": + raise RuntimeError("FITS data store \""+self.m_prodname+"\" not available. Run csiactdata to get a list of available storage names") + if not gammalib.is_fits(self.m_obs_index+"[OBS_INDEX]"): + raise RuntimeError("Observation index file \""+self.m_obs_index+"[OBS_INDEX]\" for FITS data store \""+self.m_prodname+"\" not available. Check your master index file or run csdsinfo to get a list of available storage names.") + + self.m_log = False # Logging in client tools + self.m_debug = False # Debugging in client tools + self.m_clobber = self["clobber"].boolean() + + # Return + return + + def obs_ids(self): + """ + Return OBS IDs + """ + + # Return + return self.runs + + def execute(self): + """ + Execute the script. + """ + # Run the script + self.run() + + # Save residual map + self.save(self.m_outfile) + + # Return + return + + + 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() + + # Write input parameters into logger + if self.logTerse(): + self.log_parameters() + self.log("\n") + + # Initialise run list + self.runs = [] + + # Initialise selction expression + expr = "" + + # Add spatial expression if possible + if self.select_radec: + expr += "ANGSEP("+str(self.m_ra)+","+str(self.m_dec)+",RA_PNT,DEC_PNT)<="+str(self.m_radius) + + # Add connector if expression is empty + if len(expr): + expr += "&&" + + # Add quality expression + expr += "QUALITY<="+str(self.m_quality) + + # Add user expression + if self.m_expression == "NONE" or len(self.m_expression) > 0: + expr += "&&"+self.m_expression + + # Initialise filename + openfile = self.m_obs_index+"[OBS_INDEX]["+expr+"]" + + # Open file + fits = gammalib.GFits(openfile) + + # Check for extistence of rows + if fits["OBS_INDEX"].nrows() > 0: + + # Get HDU + obs_indx = fits["OBS_INDEX"] + + # Read observation IDs into array + for i in range(obs_indx.nrows()): + self.runs.append(obs_indx["OBS_ID"][i]) + + if self.logNormal(): + self.log("\n") + self.log.header3("Expression") + self.log(expr) + self.log("\n") + + + if self.logTerse(): + self.log("\n") + self.log.header3("Observations") + self.log("Found "+str(len(self.runs))+" observations") + self.log("\n") + + # Close file + fits.close() + + # Return + return + + def save(self, outfile): + + # Check for clobber + if os.path.isfile(outfile) and not self.clobber(): + self.log("File "+outfile+" already exists. Set clobber=yes to overwrite file") + self.log("\n") + else: + # Write runs to file + f = open(outfile,"w") + for run in self.runs: + f.write(str(run)+" \n") + f.close() + + # Return + return + +# ======================== # +# Main routine entry point # +# ======================== # +if __name__ == '__main__': + """ + Generates residual count map. + """ + # Create instance of application + app = csfindobs(sys.argv) + + # Open logfile + app.logFileOpen() + + # Execute application + app.execute() + diff --git a/cscripts/csiactdata.py b/cscripts/csiactdata.py index bb87447..afeb50b 100755 --- a/cscripts/csiactdata.py +++ b/cscripts/csiactdata.py @@ -110,7 +110,11 @@ class csiactdata(ctools.cscript): # Get filename of master index file self.m_master_indx = self["master_indx"].string() self.m_master_file = os.path.join(self.datapath, self.m_master_indx) - + + # Check for presence of master index file + if not os.path.isfile(self.m_master_file): + raise RuntimeError("Master index file \""+self.m_master_file+"\" not found. Use hidden parameter \"master_indx\" to specifiy a different filename.") + # Return return diff --git a/cscripts/csiactobs.py b/cscripts/csiactobs.py index 0ba1d0c..7a06faa 100755 --- a/cscripts/csiactobs.py +++ b/cscripts/csiactobs.py @@ -93,19 +93,20 @@ class csiactobs(ctools.cscript): # Create default parfile pars = gammalib.GApplicationPars() pars.append(gammalib.GApplicationPar("datapath","s","a",self.datapath,"","","Path were data is located")) - pars.append(gammalib.GApplicationPar("prodname","s","a","","","","Data storage name (Run csdsinfo to view your options)")) + pars.append(gammalib.GApplicationPar("prodname","s","a","","","","Data storage name (Run csiactdata to view your options)")) pars.append(gammalib.GApplicationPar("runlistfile","f","a","runlist.lis","","","Runlist file")) pars.append(gammalib.GApplicationPar("bkgpars","i","a","1","","","Number of free parameters per background model")) pars.append(gammalib.GApplicationPar("outobs","f","a","obs.xml","","","Observation XML outfile")) pars.append(gammalib.GApplicationPar("outmodel","f","a","bgmodels.xml","","","Output model XML file")) pars.append(gammalib.GApplicationPar("inmodel","f","h","NONE","","","Input model XML file (optional)")) pars.append(gammalib.GApplicationPar("master_indx","s","h","master.json","","","Name of master index file")) + pars.append(gammalib.GApplicationPar("bkg_scale","b","h","yes","","","Specifies whether the background scaling factor from the observation index file should be applied if available. ")) pars.append(gammalib.GApplicationPar("ev_hiera","s","h","events","","","Hierarchy of event formats")) pars.append(gammalib.GApplicationPar("aeff_hiera","s","h","aeff_2d","","","Hierarchy of effective area formats")) pars.append(gammalib.GApplicationPar("psf_hiera","s","h","psf_king|psf_3gauss","","","Hierarchy of psf formats")) pars.append(gammalib.GApplicationPar("edisp_hiera","s","h","edisp_2d","","","Hierarchy of energy dispersion formats")) pars.append(gammalib.GApplicationPar("bkg_hiera","s","h","bkg_3d","","","Hierarchy of background formats")) - pars.append(gammalib.GApplicationPar("bkg_mod_hiera","s","h","irf","irf|aeff|gauss","","Hierarchy of background models")) + pars.append(gammalib.GApplicationPar("bkg_mod_hiera","s","h","irf|aeff|gauss","","","Hierarchy of background models")) pars.append(gammalib.GApplicationPar("bkg_gauss_norm","r","h","1e-8","","","Input normalisation for Gaussian background")) pars.append(gammalib.GApplicationPar("bkg_gauss_index","r","h","-2.0","","","Input spectral index for Gaussian background")) pars.append(gammalib.GApplicationPar("bkg_gauss_sigma","r","h","2.5","","","Input sigma for Gaussian background")) @@ -144,6 +145,9 @@ class csiactobs(ctools.cscript): # Master index file name self.m_master_indx = self["master_indx"].string() + # Read flag for background scaling factor + self.m_use_bkg_scale = self["bkg_scale"].boolean() + # Read hierarchy of file loading self.m_ev_hiera = self["ev_hiera"].string().split("|") self.m_aeff_hiera = self["aeff_hiera"].string().split("|") @@ -186,17 +190,19 @@ class csiactobs(ctools.cscript): # Check HDUs if self.m_hdu_index == "" or self.m_obs_index == "": - raise RuntimeError("FITS data store \""+self.m_prodname+"\" not available. Run csdsinfo to get a list of available storage names") + raise RuntimeError("FITS data store \""+self.m_prodname+"\" not available. Run csiactdata to get a list of available storage names") if not gammalib.is_fits(self.m_hdu_index+"[HDU_INDEX]"): - raise RuntimeError("HDU index file \""+self.m_hdu_index+"[HDU_INDEX]\" for FITS data store \""+self.m_prodname+"\" not available. Check your master index file or run csdsinfo to get a list of available storage names.") - - # Load index - if gammalib.is_fits(self.m_obs_index+"[OBS_INDEX]"): - fits = gammalib.GFits(self.m_obs_index) - self.has_bkg_scale = fits["OBS_INDEX"].contains("BKG_SCALE") - fits.close() - else: - self.has_bkg_scale = False + raise RuntimeError("HDU index file \""+self.m_hdu_index+"[HDU_INDEX]\" for FITS data store \""+self.m_prodname+"\" not available. Check your master index file or run csiactdata to get a list of available storage names.") + + # Check for existence of "BKG_SCALE" in the observation index file if required + if self.m_use_bkg_scale: + if gammalib.is_fits(self.m_obs_index+"[OBS_INDEX]"): + fits = gammalib.GFits(self.m_obs_index) + if not fits["OBS_INDEX"].contains("BKG_SCALE"): + self.m_use_bkg_scale = False + fits.close() + else: + self.m_use_bkg_scale = False # Create base data directory from hdu index file location self.subdir = os.path.dirname(self.m_hdu_index) @@ -481,7 +487,7 @@ class csiactobs(ctools.cscript): # Handle background scale information if available bkg_scale = 1.0 - if self.has_bkg_scale: + if self.m_use_bkg_scale: obsindx = gammalib.GFits(self.m_obs_index+"[OBS_INDEX]"+obs_selection) bkg_scale = obsindx["OBS_INDEX"]["BKG_SCALE"][0] obsindx.close() @@ -527,7 +533,7 @@ class csiactobs(ctools.cscript): self.log(" Point spread function: "+psffile+"\n") self.log(" Energy dispersion: "+edispfile+"\n") self.log(" Background file: "+bkgfile+"\n") - if self.has_bkg_scale: + if self.m_use_bkg_scale: self.log(" Background scale: "+str(bkg_scale)+"\n") self.log("\n") diff --git a/doc/source/reference_manual/csfindobs.rst b/doc/source/reference_manual/csfindobs.rst new file mode 100644 index 0000000..a141d16 --- /dev/null +++ b/doc/source/reference_manual/csfindobs.rst @@ -0,0 +1,87 @@ +.. _csfindobs: + +csfindobs +========= + +Creates a list of observation IDs that fulfil certain criteria, e.g. pointing close to specific source + + +Synopsis +-------- + +This script can be used to create lists of observation IDs that share certain properties. For +instance, all observations pointing around a specific sky position can be selected. In addition, +the script is flexible to use expressions to further constrain criteria to select observations. +The user can e.g. select all observations within a certain zenith angle range by specifying the hidden +parameter expression: e.g. use ``csfindobs expression="ZEN_PNT<50&&ZEN_PNT>40"`` to constrain the zenith angle range +from 40 to 50 degrees. The output of this script is an ASCII file containing observation IDs that fulfil criteria +specified by the user. This file can be used as input for :doc:`csiactobs`. The script operates +on an observation index file that needs to be provided along with IACT data. `See here `__ how IACT data +should be structured. To investigate additional parameters which are available for selection, +have a look `here `__. + + +General parameters +------------------ + +``datapath [string]`` + Path were data is located + +``prodname [string]`` + Name of FITS production (Run :doc:`csiactdata` to view your options) + +``ra [real]`` + Right ascension of search region (invoke "INDEF" to avoid spatial selection) + +``dec [real]`` + Declination of search region (invoke "INDEF" to avoid spatial selection) + +``rad [real]`` + Radius of search region (deg) (invoke "INDEF" or negative values to avoid spatial selection) + +``outfile [file]`` + Runlist outfile + +``(min_qual = 0) [integer]`` + Minimum data quality (0=best, 1=medium, 2=bad) + +``(expression = NONE) [string]`` + Additional expression + +``(master_index = master.json) [string]`` + Name of master index file + + + +Standard parameters +------------------- + +``(chatter = 2) [integer]`` + Verbosity of the executable: + chatter = 0: no information will be logged + + chatter = 1: only errors will be logged + + chatter = 2: errors and actions will be logged + + chatter = 3: report about the task execution + + chatter = 4: detailed report about the task execution + +``(clobber = yes) [boolean]`` + Specifies whether an existing output counts cube should be overwritten. + +``(debug = no) [boolean]`` + Enables debug mode. In debug mode the executable will dump any log file output to the console. + +``(mode = ql) [string]`` + Mode of automatic parameters (default is "ql", i.e. "query and learn"). + +``(logfile = csfindobs.log) [filename]`` + Log filename. + + +Related tools or scripts +------------------------ + +None \ No newline at end of file diff --git a/doc/source/reference_manual/csiactdata.rst b/doc/source/reference_manual/csiactdata.rst new file mode 100644 index 0000000..141c98a --- /dev/null +++ b/doc/source/reference_manual/csiactdata.rst @@ -0,0 +1,59 @@ +.. _csiactdata: + +csiactdata +=========== + +Dumps information about the available FITS productions of IACT data on the screen + + +Synopsis +-------- + +This script inspects the local IACT FITS data storage and prints the names of +available and valid data productions on the screen. The first parameter ``datapath`` +is only queried if the environment variable $VHEFITS is not set. The path should be set +to the location of the `master index file `__. +The output of this script can be used as input for further tools that use IACT data. + +General parameters +------------------ + +``datapath [string]`` + Path were data is located + +``(master_index = master.json) [string]`` + Name of master index file + + +Standard parameters +------------------- + +``(chatter = 2) [integer]`` + Verbosity of the executable: + chatter = 0: no information will be logged + + chatter = 1: only errors will be logged + + chatter = 2: errors and actions will be logged + + chatter = 3: report about the task execution + + chatter = 4: detailed report about the task execution + +``(clobber = yes) [boolean]`` + Specifies whether an existing output counts cube should be overwritten. + +``(debug = no) [boolean]`` + Enables debug mode. In debug mode the executable will dump any log file output to the console. + +``(mode = ql) [string]`` + Mode of automatic parameters (default is "ql", i.e. "query and learn"). + +``(logfile = csiactdata.log) [filename]`` + Log filename. + + +Related tools or scripts +------------------------ + +None \ No newline at end of file diff --git a/doc/source/reference_manual/csiactobs.rst b/doc/source/reference_manual/csiactobs.rst new file mode 100644 index 0000000..2cc5f23 --- /dev/null +++ b/doc/source/reference_manual/csiactobs.rst @@ -0,0 +1,131 @@ +.. _csiactobs: + +csiactobs +=========== + +Creates an observation XML file for IACTs using a list of observation IDs as input + + +Synopsis +-------- + +This script creates an observation XML file from a list of observation IDs provided in an ASCII file. +Such an ASCII file can for instance be created with :doc:`csfindobs`. The script operates on IACT data +that needs to be present on the user machine. `See here `__ how IACT data +should be structured. + +Besides creating an observation XML file, this script also stores a model XML file that contains background models for each +observation. The number of free parameters is steered via the ``bkgpars`` parameter. + +The user can also specify as ``inmodel`` parameter an XML model file with e.g. sky models. The output model +will then contain these models as well. + +The script further allows to specify hierarchies for the data formats and background models. The program will +pick the first available option and fall back to the next one if the first choice is unavailable. +If e.g. the IRF background model is not available, the script will assign the next background model in the hierarchy ``bkg_mod_hiera``. +If the user wants to use a Gaussian background model as first choice, the parameter ``bkg_mod_hiera`` should be specified to "gauss". +In addition, the user can specify start parameters for the background parameters that will be in the output XML model. + +If an observation that given in the input ASCII file is not available on the user machine, the script dumps a warning into the logfile +and the obsservation is skipped. + + +General parameters +------------------ + +``datapath [string]`` + Path were data is located + +``prodname [string]`` + Name of FITS production (Run :doc:`csiactdata` to view your options) + +``runlistfile [file]`` + Input list of observation IDs + +``bkgpars [integer]`` + Number of free parameters for each background model + +``outobs [file]`` + Observation XML outfile + +``outmodel [file]`` + Output model XML file + +``(inmodel = NONE) [file]`` + Input model XML file (optional) + +``(master_indx = master.json) [file]`` + Name of master index file + +``(bkg_scale = yes) [boolean] + Specifies whether the background scaling factor from the observation index file should be applied if available. + +``(ev_hiera = events) [string]`` + Hierarchy of event file formats + +``(aeff_hiera = aeff_2d) [string]`` + Hierarchy of effective area formats + +``(psf_hiera = psf_king|psf_3gauss) [string]`` + Hierarchy of psf formats + +``(edisp_hiera = edisp_2d) [string]`` + Hierarchy of energy disperson formats + +``(bkg_hiera = bkg_3d) [string]`` + Hierarchy of background formats. + +``(bkg_mod_hiera = irf|aeff|gauss) [string]`` + Hierarchy of background models + +``(bkg_gauss_norm = 1e-8) [real]`` + Input normalisation for Gaussian background + +``(bkg_gauss_index = -2.0) [real]`` + Input spectral index for Gaussian background + +``(bkg_gauss_sigma = 1e-8) [real]`` + Input sigma for Gaussian background + +``(bkg_aeff_norm = 1e-14) [real]`` + Input normalisation for effective area background + +``(bkg_aeff_index = -2.0) [real]`` + Input spectral index for effective area background + +``(bkg_range_factor = 100.0) [real]`` + Factor to determine range of background normalisation + + +Standard parameters +------------------- + +``(chatter = 2) [integer]`` + Verbosity of the executable: + chatter = 0: no information will be logged + + chatter = 1: only errors will be logged + + chatter = 2: errors and actions will be logged + + chatter = 3: report about the task execution + + chatter = 4: detailed report about the task execution + +``(clobber = yes) [boolean]`` + Specifies whether an existing output counts cube should be overwritten. + +``(debug = no) [boolean]`` + Enables debug mode. In debug mode the executable will dump any log file output to the console. + +``(mode = ql) [string]`` + Mode of automatic parameters (default is "ql", i.e. "query and learn"). + +``(logfile = csiactobs.log) [filename]`` + Log filename. + + +Related tools or scripts +------------------------ + +None \ No newline at end of file diff --git a/doc/source/reference_manual/index.rst b/doc/source/reference_manual/index.rst index 86cc9ed..6e4fe82 100644 --- a/doc/source/reference_manual/index.rst +++ b/doc/source/reference_manual/index.rst @@ -50,3 +50,6 @@ cscripts csmodelmerge --- Merges several model containers into one file cstsmapmerge --- Merges slices from ts map computations csobs2caldb --- Creates a caldb entry from an input observation + csiactdata --- Dumps information about the IACT data available on the user machine + csiactobs --- Creates an observation XML file for IACTs using a list of observation IDs as input + csfindobs --- Creates a list of observation IDs diff --git a/test/iactdata/runlist.txt b/test/iactdata/runlist.txt deleted file mode 100644 index 573541a..0000000 --- a/test/iactdata/runlist.txt +++ /dev/null @@ -1 +0,0 @@ -0 diff --git a/test/test_cscripts.sh.in b/test/test_cscripts.sh.in index 1a81df7..25745b7 100755 --- a/test/test_cscripts.sh.in +++ b/test/test_cscripts.sh.in @@ -119,6 +119,29 @@ $ECHO " ok" # +# Test csfindobs +# ============== +$ECHO -n "Test csfindobs: " +chmod +x @abs_top_srcdir@/cscripts/csfindobs.py +@abs_top_srcdir@/cscripts/csfindobs.py \ + datapath="iactdata/" \ + prodname="unit-test" \ + ra="83.6331" \ + dec="22.01" \ + rad="1.0" \ + outfile="iactdata/runlist.txt" > csfindobs.log +$ECHO -n "." +if [ -s "iactdata/runlist.txt" ] +then + $ECHO -n "." +else + $ECHO " iactdata/runlist.txt file is not valid" + exit 1 +fi +$ECHO " ok" + + +# # Test csiactobs # ============== $ECHO -n "Test csiactobs: "