# Library of utility FUNCTIONS common to several modules.
package ModuleUtil;
use strict;
use English;
use Carp;
use ModuleResources;
use FITSUtils;
use File::Temp;
use Regexp::Common;
use List::Util qw(min max);
use vars qw( $VERSION );
$VERSION = 1.0;
#----------------------------
# output pattern of fimgstat
#----------------------------
#The sum of the selected image = 118590.000000
#The mean of the selected image = 0.282422
#The standard deviation of the selected image = 19.612485
#The number of points used in calculation = 419904
#The minimum of selected image = 0.0
#The maximum of selected image = 5573.0
#The location of minimum is at pixel number = (1,1)
#The location of maximum is at pixel number = (325,322)
use vars qw( @Fimgstat_Patterns );
# Using an array rather than a hash because the input data is ordered
# so this will be quicker.
@Fimgstat_Patterns = (
sum => qr/^The sum of the selected image\s*?=\s*?$RE{num}{real}{-keep}\s*$/
,mean => qr/^The mean of the selected image\s*?=\s*?$RE{num}{real}{-keep}\s*$/
,stddev => qr/^The standard deviation of the selected image\s*?=\s*?$RE{num}{real}{-keep}\s*$/
,count => qr/^The number of points used in calculation\s*?=\s*?$RE{num}{int}{-keep}\s*?/
,min => qr/^The minimum of selected image\s*?=\s*?$RE{num}{real}{-keep}\s*$/
,max => qr/^The maximum of selected image\s*?=\s*?$RE{num}{real}{-keep}\s*$/
,minpos => qr/^The location of minimum is at pixel number\s*?=\s*?\(($RE{num}{int},$RE{num}{int})\)\s*$/
,maxpos => qr/^The location of maximum is at pixel number\s*?=\s*?\(($RE{num}{int},$RE{num}{int})\)\s*$/
);
#------------------------------
# output pattern of fstatistic
#------------------------------
# The sum of the selected column is 0.0000000
# The mean of the selected column is 0.0000000
# The standard deviation of the selected column is 0.0000000
# The minimum of selected column is 0.0000000
# The maximum of selected column is 0.0000000
# The number of points used in calculation is 4347
use vars qw( @Fstatistic_Patterns );
# Using an array rather than a hash because the input data is ordered
# so this will be quicker.
@Fstatistic_Patterns = (
sum => qr/^\s*The sum of the selected column is\s*$RE{num}{real}{-keep}\s*$/
,mean => qr/^\s*The mean of the selected column is\s*$RE{num}{real}{-keep}\s*$/
,stddev => qr/^\s*The standard deviation of the selected column is\s*$RE{num}{real}{-keep}\s*$/
,min => qr/^\s*The minimum of selected column is\s*$RE{num}{real}{-keep}\s*$/
,max => qr/^\s*The maximum of selected column is\s*$RE{num}{real}{-keep}\s*$/
,count => qr/^\s*The number of points used in calculation is\s*?$RE{num}{int}{-keep}\s*?/
);
sub isSourceOnChip
{
# Use echeckregion to determine if a source is within the detector window or not.
# Sometimes a source extraction region clips a chip giving enough events
# to pass the .non-zero' events test but there is no RMF as the source is
# in a void.
# Returns : 1 = onchip, 0 = offchip
my ($srcexp , $table , $output) = @_;
my $status = &doCommand(
'echeckregion'
, srcexp => $srcexp
, table => $table
, output => $output
) or exception();
my $rtn = 0;
if ( &fileExists( file => $output ) )
{
my $fh;
open($fh , "<$output")
or Exception->error('FILESYSTEM','Unable to open echeckregion output file.' , $output )
;
my @tmp = grep /Status\s+of\s+the\s+centre\s+of\s+the\s+input\s+region\s+is:/,(<$fh>);
close($fh);
if (@tmp)
{
$tmp[0] =~ /Status\s+of\s+the\s+centre\s+of\s+the\s+input\s+region\s+is:\s*(\d+)/;
$rtn = $1;
}
# else assume the answr is no (0).
}
else
{
Msg->info("Unable to find output from echeckregion.");
exception();
}
return $rtn;
}
sub parseFimgstatOutput
{
my $f = shift;
Exception->error('BADMETHODCALL','Called with non-existant file: ' , $f )
unless ( -f $f )
;
# Search the output from fimgstat for what we want
my $fh;
open($fh , "<$f")
or Exception->error('FILESYSTEM','Unable to open fimgstat output file.' , $f )
;
my @tmpstats = <$fh>;
close($fh);
my %h;
my $i = 0;
foreach my $line (@tmpstats)
{
for ( ; $i< @Fimgstat_Patterns ; $i+=2 )
{
my @q = $line =~ $Fimgstat_Patterns[$i+1];
next unless @q;
if ($q[0] !~ /,/)
{
$h{ $Fimgstat_Patterns[$i] } = $q[0];
}
else
{
$h{ $Fimgstat_Patterns[$i] } = [ split /,/,$q[0] ];
}
last;
}
}
return %h;
}
sub getImageStats
{
# Call fimgstat and put the results into a hash
my ($imgFile , $statFile) = @_;
$statFile ||= File::Temp->new()->filename();
unless ( -f $statFile)
{
&doCommand (
'fimgstat', infile => "$imgFile"
, threshlo => 'INDEF'
, threshup => 'INDEF'
, outfile => "$statFile"
, clobber => 'Y'
, mode => 'ql'
, sum => 0.0
, mean => 0.0
, rms => 0.0
, num => 0
, min => 0.0
, max => 0.0
, xmin => 0
, ymin => 0
, xmax => 0
, ymax => 0
) or exception()
;
}
my %h = &parseFimgstatOutput( $statFile );
return %h;
}
sub isImageEmpty
{
# get context for error reporting.
#
# Use fimgstat to determine if a file has sufficient range for implot.
# Return 1 = empty |0 = not empty
#
my ($imgFile , $statFile) = @_;
my $isEmpty = 0;
my %h = &getImageStats($imgFile,$statFile);
unless (defined $h{max} && defined $h{min} )
{
# We didn't find what we expected to find.
Exception->error('FIMGSTAT','Unable to parse fimgstat output.');
}
if ( !$h{max} || ( $h{max} - $h{min} ) <= 1.0 )
{
Msg->info("The image $imgFile is empty.");
$isEmpty = 1;
}
return $isEmpty;
}
sub isImageLimit
{
#
# Use fimgstat to determine if image has sufficient counts for subsequent analysis
# Return 1 = below limit |0 = equal or above limit
# $imgLimit is number of limiting counts
my ($imgFile , $statFile, $imgLimit) = @_;
my $isLimit = 0;
my %h = &getImageStats($imgFile,$statFile);
unless (defined $h{max} && defined $h{min} )
{
# We didn't find what we expected to find.
Exception->error('FIMGSTAT','Unable to parse fimgstat output.');
}
if ( $h{sum} < $imgLimit )
{
my $sum = $h{sum};
Msg->info("The image $imgFile has $sum total counts, below limit $imgLimit");
$isLimit = 1;
}
return $isLimit;
}
sub insertdb
{
# Attatch an arbitrary data structure to the sequence database.
# The structure can be retrieved using the given key.
# Once created given key,value pair become read only.
# %d = ()
my ($key,$data) = (@_);
# Turn an array reference key into a usable key
if (UNIVERSAL::isa($key,'ARRAY') )
{
$key = join(':',@$key);
}
if (exists $ModuleResources::Seqdb->{_moduledb}{$key})
{
Exception->error("Attempt to amend read-only module database entry");
}
$ModuleResources::Seqdb->{_moduledb}{$key} = Storable::dclone($data);
return $key;
}
sub selectdb
{
# Attatch an arbitrary data structure to the sequence database.
# The structure can be retrieved using the given key.
# Once created given key,value pair become read only.
# %d = ()
my ($key,$data) = (@_);
# Turn an array reference key into a usable key
if ( UNIVERSAL::isa($key,'ARRAY') )
{
$key = join(':',@$key);
}
if (exists $ModuleResources::Seqdb->{_moduledb}{$key})
{
return $ModuleResources::Seqdb->{_moduledb}{$key};
}
return undef;
}
sub keepSafe
{
# %d = ( file_identifier => [] , text => [] )
# fileid == enough to find an existing file or create a new one.
my %d = (@_);
my $rtn = 0;
my $safe = findFile( @{$d{file_identifier}} );
unless ( $safe && fileExists($safe) )
{
$safe = newFile( @{$d{file_identifier}} );
}
if ($safe)
{
my @d = @{ $d{text} };
foreach my $d (@d) { $d .= "\n"; }
writeASCIIFile( name => $safe
, text => \@d
, append => 1
);
$rtn = 1;
}
return $rtn;
}
sub ontimeFix
{
my ($output , @image) = (@_);
my @ontime = qw(
ONTIME01
ONTIME02
ONTIME03
ONTIME04
ONTIME05
ONTIME06
ONTIME07
ONTIME08
ONTIME09
ONTIME10
ONTIME11
ONTIME12
);
Msg->mark();
my %ontime = ();
my $exposure = 0;
foreach my $img (@image)
{
next unless -f $img;
foreach my $kw ( @ontime )
{
my @opt = ($img,'PRIMARY',$kw);
next unless FITSUtils::hasKeyword(@opt);
my $v = FITSUtils::readKeyword(@opt);
$ontime{$kw} += FITSUtils::readKeyword(@opt);;
if ($exposure < $ontime{$kw})
{
$exposure = $ontime{$kw};
}
}
}
unless (keys %ontime)
{
Msg->info("No ONTIME values in input images.");
}
return (%ontime , EXPOSURE => $exposure);
}
sub pnLowGainWarn
{
my ( $file , $h ) = @_;
my @ext = qw(
EXPOSU01
EXPOSU02
EXPOSU03
EXPOSU04
EXPOSU05
EXPOSU06
EXPOSU07
EXPOSU08
EXPOSU09
EXPOSU00
EXPOSU11
EXPOSU12
);
my @warn;
foreach my $ext ( @ext )
{
next unless ( FITSUtils::hasExtension( $file , $ext ) );
foreach my $kw (qw( GAINCAME WARN_106 ))
{
my @opt = ( $file , $ext , $kw );
if ( FITSUtils::hasKeyword( @opt ) )
{
$h->{$ext}{$kw} = FITSUtils::readKeyword(@opt);
}
}
push @warn , $ext if ( ($h->{$ext}{GAINCAME} && $h->{$ext}{GAINCAME} eq 'LOW' )
&& ( $h->{$ext}{WARN_106} && $h->{$ext}{WARN_106} =~ /CCDlowGain/i )
);
}
return @warn;
}
sub select_rgssources
{
my ($file , $extension , $haveEpicList , $rate_cutoff ) = (@_);
# Select additional EPIC sources for spectral extraction from the RGS source list.
# Returns a source selection expression for passing to rgsregions.
# Also works
my $primesrc = readFITSKeyword(
file => $file
, extension => $extension
, keyword => "PRIMESRC"
);
my @idx;
# Only if we are playing with an EPIC source list.
# Doesn't make sense otherwise.
if ( $haveEpicList )
{
my $data = readFITSTable(
file => $file
, extension => $extension
, colname => [qw( INDEX LABEL RATE FLAG )]
);
my $processables = 0;
my $hasepic = 0;
for my $i ( 0 .. $data->{-nrows} -1 )
{
# Skipables.
# avoid where:
# FLAG set
# avoid duplicating the PRIMESRC
# too faint.
# non-EPIC source (PROPOSAL, ONAXIS , WHOLEFIELD etc)
# Make sure it is processable.
next if ( ( $data->{FLAG}[$i] ne 0 )
|| ( $data->{LABEL}[$i] !~ /EPIC/ )
);
# Record the fact we have any EPIC sources at all.
$hasepic ||= 1;
next unless ( $data->{RATE}[$i] > $rate_cutoff );
# Count EPIC sources which pass the above.
$processables++;
# but avoid adding EPIC PRIMESRC to the list.
next if ( $data->{INDEX}[$i] eq $primesrc );
push @idx, $i;
}
# Sort the list of indexes on RATE
@idx = sort { $data->{RATE}[$a] <=> $data->{RATE}[$b] } @idx;
#convert @idx to INDEX column values.
@idx = map { $data->{INDEX}[$_]; } @idx;
doCommand('addattribute'
, set => "$file:SRCLIST"
, attributename => [ 'EPMINRTE' , 'EPRGSRCN' ]
, attributetype => [ 'real' , 'integer' ]
, attributecomment => [
'EPIC source RATE Threshold'
,'Number of EPIC source within RGS FOV and RATE > EPMINRTE and FLAG 0'
]
,integervalue => $processables
,realvalue => $rate_cutoff
) or exception();
}
my @rtn = (@idx)
? ( "INDEX==#PRIMESRC || INDEX==$idx[-1]" , $primesrc , $idx[-1] )
: ( "INDEX==#PRIMESRC" , $primesrc )
;
return @rtn;
}
sub get_rgssources_param
{
my ($file, $extension , $instrument) = (@_);
# Select sources from RGS source list so details can be recorded in the source spectra
my $primesrc = readFITSKeyword( file => $file
, extension => $extension
, keyword => "PRIMESRC"
);
my %rtn;
my $data = readFITSTable(
file => $file
, extension => $extension
, colname => [qw( INDEX LABEL RA DEC RATE PROCESS FLAG )]
);
my $processables = 0;
my $hasepic = 0;
for my $i ( 0 .. $data->{-nrows} -1 )
{
# Skipables.
# avoid where:
# FLAG set
# avoid duplicating the PRIMESRC
# too faint.
# non-EPIC source (PROPOSAL, ONAXIS , WHOLEFIELD etc)
# Make sure it is processable.
next if ( ( $data->{PROCESS}[$i] =~ /$RE{boolean}{false}/i )
|| ( ! hasFITSExtension( file => $file
, extension => "${instrument}_SRC".$data->{INDEX}[$i]."_SPATIAL"
)));
# Keep EPIC source details to be recorded in the spectrum headers.
$rtn{ $data->{INDEX}[$i] } = {
INDEX => $data->{INDEX}[$i]
,LABEL => $data->{LABEL}[$i]
,RA => $data->{RA}[$i]
,DEC => $data->{DEC}[$i]
,RATE => $data->{RATE}[$i]
,PROCESS => $data->{PROCESS}[$i]
,FLAG => $data->{FLAG}[$i]
};
}
return %rtn;
}
sub parseFstatisticOutput
{
my $f = shift;
Exception->error('BADMETHODCALL','Called with non-existant file: ' , $f )
unless ( -f $f )
;
# Search the output from fstatistic for what we want
my $fh;
open($fh , "<$f")
or Exception->error('FILESYSTEM','Unable to open fstatistic output file.' , $f )
;
my @tmpstats = <$fh>;
close($fh);
my %h;
my $i = 0;
foreach my $line (@tmpstats)
{
for ( ; $i< @Fstatistic_Patterns ; $i+=2 )
{
my @q = $line =~ $Fstatistic_Patterns[$i+1];
next unless @q;
if ($q[0] !~ /,/)
{
$h{ $Fstatistic_Patterns[$i] } = $q[0];
}
else
{
$h{ $Fstatistic_Patterns[$i] } = [ split /,/,$q[0] ];
}
last;
}
}
return %h;
}
sub isColumnEmpty
{
# get context for error reporting.
#
# Use fstatistic to determine if a table column is identically 0
# Return 1 = empty |0 = not empty
#
my ($inFile , $colName, $statFile) = @_;
my $isEmpty = 0;
$statFile ||= File::Temp->new()->filename();
unless ( -f $statFile)
{
&doCommand (
'fstatistic', infile => "$inFile"
, colname => "$colName"
, rows => "-"
, outfile => "$statFile"
) or exception()
;
}
my %h = &parseFstatisticOutput( $statFile );
unless (defined $h{max} && defined $h{min} )
{
# We didn't find what we expected to find.
Exception->error('FSTATISTIC','Unable to parse fstatistic output.');
}
if ( ($h{max}==0) && ($h{min}==0) )
{
Msg->info("The column $colName of $inFile is identically 0.");
$isEmpty = 1;
}
return $isEmpty;
}
sub hasColumnNegValues
{
#
# Check if some value in column is negative
# Return 1 = Column[x] < 0 | 0 otherwise
#
# Remove NaN's before min test
my ($file, $ext, $colname) = @_;
my $colHasNegVal = 0;
my $columnValues = readFITSColumn(
file => $file
, extension => $ext
, column => $colname
);
# Remove NaN's from columnValues before getting min value
my @columnValuesNew;
foreach my $val (@$columnValues){
push(@columnValuesNew, $val) unless ($val =~/NaN/i);
}
if (@columnValuesNew){
my $columnValuesMin = min(@columnValuesNew);
if ( $columnValuesMin < 0 ){ $colHasNegVal = 1; }
} else {$colHasNegVal = 1;}
return $colHasNegVal;
}
# Modules like ExpDetect were not originally set up for mosaics,
# although in principle they might be OK if the individual pointings
# are all very close to one another. This subroutine simply checks a
# reference image to determine whether its angular extent is larger
# than 0.82 deg along either axis as a simple test of whether it is a
# large mosaic or not.
#
# Limit size as input parameter ($sizeLimit). Default value: 0.82 deg (JVP)
sub isSmallMap
{
#my $imFile = $_[0];
my ($imFile , $sizeLimit) = @_;
if (!$sizeLimit){ $sizeLimit = 0.82; }
my $status = 0;
return $status unless $imFile;
#my $cunit1 = readFITSKeyword(file => $imFile, extension => 1, keyword => 'CUNIT1');
#my $cunit2 = readFITSKeyword(file => $imFile, extension => 1, keyword => 'CUNIT2');
#
#Exception->error('Module', 'Supplied image file has the wrong axis units:', $imFile)
# unless ( ($cunit1 =~ m/deg/i) and ($cunit2 =~ m/deg/i) );
info( "Warning: not checking units since some maps are lacking CUNIT keywords (e.g., expposure maps)");
my $xsize = readFITSKeyword(file => $imFile, extension => 1, keyword => 'CDELT1') *
readFITSKeyword(file => $imFile, extension => 1, keyword => 'NAXIS1');
my $ysize = readFITSKeyword(file => $imFile, extension => 1, keyword => 'CDELT2') *
readFITSKeyword(file => $imFile, extension => 1, keyword => 'NAXIS2');
$xsize = abs($xsize);
$ysize = abs($ysize);
$status = 1 if ($xsize < $sizeLimit) and ($ysize < $sizeLimit);
info("Dimensions of the test image $imFile are $xsize x $ysize. sizeLimit: $sizeLimit. isSmallMap=$status.");
return $status;
}
# Basic statistics functions
# mean
sub mean {
my (@data) = @_;
my $sum;
foreach (@data) {
$sum += $_;
}
return ( $sum / @data );
}
# median
sub median {
my (@data) = sort { $a <=> $b } @_;
if ( scalar(@data) % 2 ) {
return ( $data[ @data / 2 ] );
} else {
my ( $upper, $lower );
$lower = $data[ @data / 2 ];
$upper = $data[ @data / 2 - 1 ];
return ( mean( $lower, $upper ) );
}
}
# Function to read all the coefficients from EPICModes.fits file
# ( 'Observing modes Pileup and OoT data' )
#
sub coefficients {
my ($file) = @_;
my $dataPileupOoT = readFITSTable(
file => $file
, extension => 'EPICModes'
, colname => [qw( INST PHSMODE MODE SASMODE A B C Deadtime pup_limit cnts_limit Modes_2 LifetimeOoT FractionOoT )]
);
my %modes_pileup_data;
for my $i ( 0 .. $dataPileupOoT->{-nrows} -1 ){
my $inst = $dataPileupOoT->{INST}[$i];
my $phsmode = $dataPileupOoT->{PHSMODE}[$i];
my $mode = $dataPileupOoT->{MODE}[$i];
my $sasmode = $dataPileupOoT->{SASMODE}[$i];
my $A = $dataPileupOoT->{A}[$i];
my $B = $dataPileupOoT->{B}[$i];
my $C = $dataPileupOoT->{C}[$i];
my $Deadtime = $dataPileupOoT->{Deadtime}[$i];
my $pup_limit = $dataPileupOoT->{pup_limit}[$i];
my $cnts_limit = $dataPileupOoT->{cnts_limit}[$i];
my $Modes_2 = $dataPileupOoT->{Modes_2}[$i];
my $LifetimeOoT = $dataPileupOoT->{LifetimeOoT}[$i];
my $FractionOoT = $dataPileupOoT->{FractionOoT}[$i];
if ( $inst =~ /E1/ || $inst =~ /E2/ || $inst =~ /E3/){
my $inst_;
if ($inst =~ /E1/){$inst_ = 'EMOS1';}
if ($inst =~ /E2/){$inst_ = 'EMOS2';}
if ($inst =~ /E3/){$inst_ = 'EPN';}
#my $inst_sasmode = $inst_ . "_" . $sasmode;
#$modes_pileup_data{$inst_sasmode} = [$A, $B, $C, $Deadtime, $pup_limit, $cnts_limit];
$modes_pileup_data{$inst_}{$sasmode}{'a'} = $A;
$modes_pileup_data{$inst_}{$sasmode}{'b'} = $B;
$modes_pileup_data{$inst_}{$sasmode}{'c'} = $C;
$modes_pileup_data{$inst_}{$sasmode}{'Deadtime'} = $Deadtime;
$modes_pileup_data{$inst_}{$sasmode}{'pup_limit'} = $pup_limit;
$modes_pileup_data{$inst_}{$sasmode}{'cnts_limit'} = $cnts_limit;
$modes_pileup_data{$inst_}{$sasmode}{'Modes_2'} = $Modes_2;
$modes_pileup_data{$inst_}{$sasmode}{'LifetimeOoT'} = $LifetimeOoT;
$modes_pileup_data{$inst_}{$sasmode}{'FractionOoT'} = $FractionOoT;
}
}
return %modes_pileup_data;
}
1;