Subs::XRTDB (version 0.0)


package Subs::XRTDB;
##############################################################################
#
# DESCRIPTION: 
#
# HISTORY: 
# HISTORY: $Log: XRTDB.pm,v $
# HISTORY: Revision 1.11  2007/03/23 13:08:52  apsop
# HISTORY: Add support for new xrt modes.
# HISTORY:
# HISTORY: Revision 1.10  2006/04/28 18:45:39  apsop
# HISTORY: Fix problem with too many warning messages from empty LOWRATE exposures.
# HISTORY:
# HISTORY: Revision 1.9  2006/04/26 20:47:05  apsop
# HISTORY: Better reporting for exposures with no event file.
# HISTORY:
# HISTORY: Revision 1.8  2006/04/24 13:28:33  apsop
# HISTORY: Skip entries with no lowrate event list.  All for either INDEF or NULL values returned from FITSfile class.
# HISTORY:
# HISTORY: Revision 1.7  2006/02/13 18:25:50  apsop
# HISTORY: Workaround problem when more than one event file is select for and exposure. Needs more permanent, accurate fix.
# HISTORY:
# HISTORY: Revision 1.6  2005/12/19 16:27:27  apsop
# HISTORY: Put quotes around object names so as not to confuse fcreate.
# HISTORY:
# HISTORY: Revision 1.5  2005/12/19 16:25:40  apsop
# HISTORY: Fix bug in flip-flop algo when testing for non-flip modes.
# HISTORY:
# HISTORY: Revision 1.4  2005/11/08 20:04:23  apsop
# HISTORY: <Previous comment bogus>Lots of changes for calculating flip-flop intervals, and other changes.
# HISTORY:
# HISTORY: Revision 1.3  2005/11/08 19:22:28  apsop
# HISTORY: Populate the TIMELIST and DATALIST hashes. Used to be an SWCheckInput.
# HISTORY:
# HISTORY: Revision 1.2  2005/08/30 14:18:10  apsop
# HISTORY: First production version.
# HISTORY:
# HISTORY: 
#
# VERSION: 0.0
#
#
##############################################################################

use strict;

use Subs::Sub;
our @ISA = ("Subs::Sub");

use Carp;
use Data::Dumper;
$Data::Dumper::Indent = 3;

use Astro::FITS::CFITSIO qw(:constants);
use Util::Date;
use Util::FITStable;
use Util::SimpleFITS;
sub new
{
    my($proto) = shift;
    my($self) = $proto->SUPER::new;
    $self->{DESCRIPTION} = 'Extracting data for XRT exposure database.';
    return $self;
}

sub body
{
    # Save arguments.
    my($self) = shift;

    # Get the helper objects.
    my($log) = $self->log or croak('No log object!');
    my($filename) = $self->filename or croak('No filename object!');
    my($procpar) = $self->procpar or croak('No procpar object!');
    my($jobpar) = $self->jobpar or croak('No jobpar object!');

    my $SLEW_GAP = 15;
    my $FLIP_END_NUM = 11;
    my @NOFLIP_MODES = (2, 3, 8, 9);
    my @MODE_NAMES = (undef, undef, 'SHORTIMA', 'LONGIMA', 'PILEDUP',
	 'LOWRATE', 'WINDOWED', 'PHOTON', 'RAWIMA', 'BIASIMA');
    my %WAVE_MODES = ( PILEDUP => { 80 => 'REFRESHPCP',
				    81 => 'PHOTONP',
				    82 => 'WINDOWEDP'},
		       LOWRATE => { 80 => 'REFRESHPCL',
				    81 => 'PHOTONL',
				    82 => 'WINDOWEDL'}
                     );

    # Set up FITS column descriptions of the fields that make up the
    # exposure database record.
    my(@db) =
	(
	 {
	     name => 'name',
	     type => '80A',
	 },
  	 {
  	     name => 'orig_target_id',
  	     type => '1J',
  	 },
  	 {
  	     name => 'target_id',
  	     type => '1J',
  	 },
	 {
	     name => 'ra',
	     type => '1D',
	 },
	 {
	     name => 'dec',
	     type => '1D',
	 },
	 {
	     name => 'roll_angle',
	     type => '1D',
	 },
	 {
	     name => 'start_time',
	     type => '22A',
	 },
	 {
	     name => 'stop_time',
	     type => '22A',
	 },
  	 {
  	     name => 'orig_obs_segment',
  	     type => '1I',
  	 },
  	 {
  	     name => 'obs_segment',
  	     type => '1I',
  	 },
  	 {
  	     name => 'orig_obsid',
  	     type => '11A',
  	 },
  	 {
  	     name => 'obsid',
  	     type => '11A',
  	 },
	 {
	     name => 'exposure',
	     type => '1D',
	 },
	 {
	     name => 'integration_time',
	     type => '1D',
	 },
 	 {
 	     name => 'window_size',
 	     type => '9A',
 	 },
	 {
	     name => 'ccdtemp',
	     type => '1D',
	 },
	 {
	     name => 'bias',
	     type => '3A',
	 },
	 {
	     name => 'operation_mode',
	     type => '10A',
	 },
	 {
	     name => 'pointing_mode',
	     type => '8A',
	 },
	 {
	     name => 'filename',
	     type => '80A',
	 },
	 {
	     name => 'flipflop',
	     type => '3A',
	 }
	 );
#    print 'db = ', Dumper \@db;

    #--------------------------------------------------------------------------

    # Create a new FITS file for the exposure database records.
    my($db) = Util::FITStable->new(\@db, log => $log, which => 'xrt');
    if (not $db) {
	$log->entry('Unable to create FITStable object!');
	return;
    }

    # Compute the path to the required XRT housekeeping file.
    my($hk_path) = $filename->get('hk', 'xrt', 'hd');
    $log->entry("hk_path = $hk_path");
    if (not $hk_path or not -f $hk_path) {
	$log->entry("Unable to locate XRT HK file $hk_path");
	return;
    }

    # Open the XRT HK file.
    my($xrt_hk) = Util::SimpleFITS->open("<$hk_path");
    if (not $xrt_hk) {
	$log->error(2, "Undefined SimpleFITS object for XRT HK file $hk_path");
	return;
    }
    my($status) = $xrt_hk->status;
    if ($status) {
	$log->error(2, "Unable to open XRT HK file $hk_path [$status]");
	return;
    }

    # Move to the FRAME extension.
    $xrt_hk->move('FRAME');
    if ($xrt_hk->status) {
 	$log->error(2, "Unable to move to FRAME extension in $hk_path");
 	return;
    }

    # Fetch the number of frames in the housekeeping file.
    my($num_frames);
    $xrt_hk->handle->get_num_rows($num_frames, $status);
    $log->entry("num_frames = $num_frames");
    if ($num_frames < 1) {
	$log->error(2, "No frames in XRT HK file $hk_path!");
	return;
    }

    #--------------------------------------------------------------------------

    # Extract required data from the job parameter file.

    # All records are for the same object.
    my($object) = $jobpar->read('object');
    $log->entry("object = $object");
    if (not $object) {
	$log->error(2, 'No object in jobpar!');
	return;
    }

    # All records are for the same original target.
    my($target) = $jobpar->read('target');
    $log->entry("target = $target");
    if (not $target) {
	$log->error(2, 'No target in jobpar!');
	return;
    }

    my($obs) = $jobpar->read('obs');
    $log->entry("obs = $obs");
    if (not $obs) {
	$log->error(2, 'No obs in jobpar!');
	return;
    }

    # All records are for the same final target.
    my($sequence) = $jobpar->read('sequence');
    $log->entry("sequence = $sequence");
    if (not $sequence) {
	$log->error(2, 'No sequence in jobpar!');
	return;
    }
    my($target_id) = substr($sequence, 0, 8);
    $log->entry("target_id = $target_id");
    if (not $target_id) {
	$log->error(2, 'Unable to compute target ID from sequence ' .
		    "$sequence!");
	return;
    }

    # Extract the observation segment from the sequence.
    my($segment) = substr($sequence, 8);
    $log->entry("segment = $segment");
    if (not defined $segment) {
	$log->error(2, 'Unable to compute segment from sequence ' .
		    "$sequence!");
	return;
    }

    # Fetch the target RA, DEC, and roll.
    my($ra) = $jobpar->read('ra');
    $log->entry("ra = $ra");
    if (not defined $ra) {
	$log->error(2, 'Undefined ra from jobpar!');
	return;
    }
    my($dec) = $jobpar->read('dec');
    $log->entry("dec = $dec");
    if (not defined $dec) {
	$log->error(2, 'Undefined dec from jobpar!');
	return;
    }
    my($roll) = $jobpar->read('roll');
    $log->entry("roll = $roll");
    if (not defined $roll) {
	$log->error(2, 'Undefined roll from jobpar!');
	return;
    }

    #--------------------------------------------------------------------------

    # Fetch required data from the frame data in the XRT housekeeping
    # file.

    # Fetch the XRTMode column to begin determination of the start and
    # end of each frame set.
    my(@XRTMode);
    $status = $xrt_hk->readcol('XRTMode' => TBYTE, [], \@XRTMode);
#    print 'XRTMode = ', Dumper \@XRTMode;
    if (not $status) {
	$log->error(2, 'Unable to fetch XRTMode column!');
	return;
    }

    # Fetch the Settled column, which is used to determine the
    # satellite observation mode.
    my(@Settled);
    $status = $xrt_hk->readcol('Settled' => TBYTE, [], \@Settled);
#    print 'Settled = ', Dumper \@Settled;
    if (not $status) {
	$log->error(2, 'Unable to fetch Settled column!');
	return;
    }

    # Fetch the In10Arcm column, which is used to determine the
    # satellite observation mode.
    my(@In10Arcm);
    $status = $xrt_hk->readcol('In10Arcm' => TBYTE, [], \@In10Arcm);
#    print 'In10Arcm = ', \@In10Arcm;
    if (not $status) {
	$log->error(2, 'Unable to fetch In10Arcm column!');
	return;
    }

    # Fetch the TIME column (seconds of the frame start time).
    my(@TIME);
    $status = $xrt_hk->readcol('TIME' => TDOUBLE, [], \@TIME);
#    print 'FSTS = ', Dumper \@FSTS;
    if (not $status) {
	$log->error(2, 'Unable to fetch TIME column!');
	return;
    }

    # Fetch the ENDTIME column (seconds of the frame end time).
    my(@ENDTIME);
    $status = $xrt_hk->readcol('ENDTIME' => TDOUBLE, [], \@ENDTIME);
#    print 'FETS = ', Dumper \@FETS;
    if (not $status) {
	$log->error(2, 'Unable to fetch ENDTIME column!');
	return;
    }

    # Fetch the PDBIASLEVEL column (a small double-precision value).
    my(@PDBIASLVL);
    $status = $xrt_hk->readcol('PDBIASLVL' => TDOUBLE, [], \@PDBIASLVL);
#    print 'PDBIASLVL = ', Dumper \@PDBIASLVL;
    if (not $status) {
	$log->error(2, 'Unable to fetch PDBIASLVL column!');
	return;
    }

    # Fetch the IMBLvl column (a small double-precision value).
    my(@IMBLvl);
    $status = $xrt_hk->readcol('IMBLvl' => TDOUBLE, [], \@IMBLvl);
#    print 'IMBLvl = ', Dumper \IMBLvl;
    if (not $status) {
	$log->error(2, 'Unable to fetch IMBLvl column!');
	return;
    }

    # Fetch the WinHalfW column (window half-width).
    my(@WinHalfW);
    $status = $xrt_hk->readcol('WinHalfW' => TINT, [], \@WinHalfW);
#    print 'WinHalfW = ', Dumper \@WinHalfW;
    if (not $status) {
	$log->error(2, 'Unable to fetch WinHalfW column!');
	return;
    }

    # Fetch the WinHalfH column (window half-height).
    my(@WinHalfH);
    $status = $xrt_hk->readcol('WinHalfH' => TINT, [], \@WinHalfH);
#    print 'WinHalfH = ', Dumper \@WinHalfH;
    if (not $status) {
	$log->error(2, 'Unable to fetch WinHalfH column!');
	return;
    }

    # Fetch the telemRow column (telemetry row count in image modes).
    my(@telemRow);
    $status = $xrt_hk->readcol('telemRow' => TINT, [], \@telemRow);
#    print 'telemRow = ', Dumper \@telemRow;
    if (not $status) {
	$log->error(2, 'Unable to fetch telemRow column!');
	return;
    }

    # Fetch the telemCol column (telemetry column count in image
    # modes).
    my(@telemCol);
    $status = $xrt_hk->readcol('telemCol' => TINT, [], \@telemCol);
#    print 'telemCol = ', Dumper \@telemCol;
    if (not $status) {
	$log->error(2, 'Unable to fetch telemCol column!');
	return;
    }

    # Fetch the WaveID column (CCD waveform ID).
    my(@WaveID);
    $status = $xrt_hk->readcol('WaveID' => TBYTE, [], \@WaveID);
#    print 'WaveID = ', Dumper \@WaveID;
    if (not $status) {
	$log->error(2, 'Unable to fetch WaveID column!');
	return;
    }
    # Fetch the CCDTemp column (CCD temperature in degrees Celsius
    # during the frame).
    my(@CCDTemp);
    $status = $xrt_hk->readcol('CCDTemp' => TFLOAT, [], \@CCDTemp);
#    print 'CCDTemp = ', Dumper \@CCDTemp;
    if (not $status) {
	$log->error(2, 'Unable to fetch CCDTemp column!');
	return;
    }

    ####################################################
    # Remove the last from from consideration if it has
    # unknown mode
    ####################################################
    $num_frames-- if $XRTMode[$num_frames-1]==0;

    ####################################################
    # Determine the event file for each frame, based on
    # the mode
    ####################################################
    my(@evt_path, @win_map, @timedel, @gti_starts, @gti_stops, $keyword, @frame_exp);
    $win_map[6] = { 50 => 'w1',
		    100 => 'w2',
		    150 => 'w3',
		    200 => 'w4',
		    250 => 'w5' };
    $win_map[7] = { 245 => 'w1',
		    250 => 'w2',
		    300 => 'w3',
		    240 => 'w4' };

    my @warnings;
    my $prev_paths = " ";
    for (my $f = 0; $f < $num_frames; $f++) {
      my $mode =$XRTMode[$f];
      my $path;
      if($mode >= 2){
	my $mode_name = (undef, undef, 'im', 'im', 'pu', 'lr', 'wt', 'pc', '', '')[$mode];
	if($mode == 6 || $mode == 7){
	  my $win_name = $win_map[$mode]->{$WinHalfW[$f]};
	  $mode_name .= $win_name ? $win_name : 'w?';
	}elsif($mode == 4 || $mode == 5){
	  if( $WaveID[$f] >= 80 && $WaveID[$f] <= 82 ){
	    my $sub_mode = ('r', 'p', 'w')[$WaveID[$f]-80];
	    $mode_name = $sub_mode . substr($mode_name,0,1);
	  }
	  if ($PDBIASLVL[$f]) {
	    $mode_name .= 'b1';
	  } else {
	    $mode_name .= 'b0';
	  }	  
	}

	if($mode >= 4 && $mode <= 7){
	  if ($Settled[$f]) {
	    $mode_name .= 'po';
	  } elsif ($In10Arcm[$f]) {
	    $mode_name .= 'st';
	  } else {
	    $mode_name .= 'sl';
	  }
	}
	my $type = (undef, undef, 'rawimage', 'rawimage',
		    'unfiltered', 'unfiltered', 'unfiltered', 'unfiltered', 
		    'xrawmode', 'xbiasmode')[$mode];

#	$log->entry("mode_name: $mode_name type: $type");
	my @paths = $filename->get($type, 'xrt', $mode_name, '*');
	if(@paths > 1){
	  my $curr_paths = join(' ', @paths);
	  if($curr_paths ne $prev_paths){
	    $log->error(1, "More than one event file found for mode $mode_name: " . $curr_paths);
	    $prev_paths = $curr_paths;
	  }
	}
	if(@paths == 0){
	  push @warnings, "No event file found for mode $mode_name";
	}
	$path = $paths[0];
      }
      #########################################################
      # Fetch required data from the header of the event file.
      #########################################################
      $timedel[$f] = 'INDEF';
      if( $path ){
	my($xrt_file) = Util::SimpleFITS->open( '<'.$path );
	$status = $xrt_file->status;
	if (not $xrt_file or $status) {
	  $log->error(1, "Unable to open XRT file $path [$status], " .
		      'skipping fetch of integration time (TIMEDEL).');
	}else{
	  my $mode = $XRTMode[$f];
	  unless( grep($mode==$_, (2,3,8,9)) ){
	    $xrt_file->move('EVENTS');
	    unless($xrt_file->status){
	      $xrt_file->handle->read_keyword('TIMEDEL', $timedel[$f], $keyword,
					      $status);
	      $timedel[$f] = 'INDEF' if $status;
	    }
	  }
	}
      }else{
	$path = 'INDEF';
      }
      $evt_path[$f] = $path;
    }

#    $log->entry('evt_path = '. Dumper \@evt_path);
#    $log->entry('timedel = '. Dumper \@timedel);

    #--------------------------------------------------------------------------

    # Compute dynamic values.

    # Compute the XRT mode string for each frame.
    my(@XRTMode_string);
    for (my $f = 0; $f < $num_frames; $f++) {
      
      $XRTMode_string[$f] = $MODE_NAMES[$XRTMode[$f]];
      if( defined $XRTMode_string[$f] ){
	my $wave_mode = $WAVE_MODES{$XRTMode_string[$f]}{$WaveID[$f]};
	$XRTMode_string[$f] = $wave_mode if $wave_mode;
      }else{
	$XRTMode_string[$f] = 'UNKNOWN';
      }
    }
#   print 'XRTMode_string = ', Dumper \@XRTMode_string;

    # Compute the values of the satellite observation mode based on
    # the Settled and In10Arcm flags.
    my(@ObservationMode);
    for (my $f = 0; $f < $num_frames; $f++) {
      if ($Settled[$f]) {
	$ObservationMode[$f] = 'pointing';
      } elsif ($In10Arcm[$f]) {
	$ObservationMode[$f] = 'settling';
      } else {
	$ObservationMode[$f] = 'slewing';
      }
    }
#   print 'ObservationMode = ', Dumper \@ObservationMode;

    # Compute the bias setting based on the XRTMode and PDBIASLVL
    # columns.
    my(@bias) = ('Y') x $num_frames;
    for (my $f = 0; $f < $num_frames; $f++) {
      if( ($XRTMode[$f] == 4 or $XRTMode[$f] == 5) 
	   and ! $PDBIASLVL[$f] ){
	$bias[$f] = 'N';
      }elsif( ($XRTMode[$f] == 2 or $XRTMode[$f] == 3) 
	   and ! $IMBLvl[$f] ){
	$bias[$f] = 'N';
      }elsif($XRTMode[$f] == 8 or $XRTMode[$f] == 9){
	$bias[$f] = 'N';
      }
    }
#    print 'bias = ', Dumper \@bias;

    # Compute the Window string for each frame.
    my(@Window);
    for (my $f = 0; $f < $num_frames; $f++) {
	if ($XRTMode[$f] == 2 or $XRTMode[$f] == 3 or $XRTMode[$f] == 8) {
	    $Window[$f] = "${telemRow[$f]}x${telemCol[$f]}";
	} elsif ($XRTMode[$f] == 4 or $XRTMode[$f] == 5) {
	    $Window[$f] = 'NA';
	} elsif ($XRTMode[$f] == 6) {
	    if ($WaveID[$f] == 6) {
		$Window[$f] = 100;
	    } elsif ($WaveID[$f] == 60) {
		$Window[$f] = 200;
	    } elsif ($Window[$f] == 61) {
		$Window[$f] = 300;
	    } elsif ($WaveID[$f] == 62) {
		$Window[$f] = 400;
	    } elsif ($WaveID[$f] == 63) {
		$Window[$f] = 500;
	    } else {
		$Window[$f] = 'INDEF';
	    }
	} elsif ($XRTMode[$f] == 7) {
	    $Window[$f] = 2 * $WinHalfW[$f] . 'x' . 2 * $WinHalfH[$f];
	} else {
	    $Window[$f] = 'INDEF';
	}
    }
#    print 'Window = ', Dumper \@Window;

    #--------------------------------------------------------------------------


    # First pass: Look for the start of a series of flip-flops. A
    # flip-flop series can start at the first frame. The first frame
    # in a flip-flop series is the frame _before_ the first
    # out-of-the-ordinary frame.

    my ($flip_mode, $flip_other) = (-1) x 2;
    $log->entry("Identifying flip-flop series start and stop frames.");
    my(@flip_flop_start) = (0) x $num_frames;
    my(@flip_flop_series_start);
    my(@flip_flop_end) = (0) x $num_frames;
    my(@flip_flop_series_end);

    for (my $f = 0; $f < $num_frames; $f++) {
      if($flip_mode == -1){
	my $this_mode = $XRTMode[$f];
	my $next_mode = $XRTMode[$f+1];
	next if (grep /^${this_mode}$/, @NOFLIP_MODES);
	next if( $next_mode &&(grep /^${next_mode}$/, @NOFLIP_MODES) );
	if ( $f < ($num_frames-2) 
	     and
	     $this_mode != $next_mode
	     and
	    ( $this_mode == $XRTMode[$f + 2] or
	      (defined($XRTMode[$f + 3]) and $this_mode == $XRTMode[$f + 3]) or
	      (defined($XRTMode[$f + 4]) and $this_mode == $XRTMode[$f + 4]) )
	   ){
#	  $log->entry("Flip-flop series starting at frame $f.");
	  $flip_flop_start[$f] = 1;
	  push(@flip_flop_series_start, $f);
	  $flip_mode = $this_mode;
	  $flip_other = $next_mode;
	}
      }else{
	my $ffend=0;
        $ffend = 1 if( ( defined($XRTMode[$f + 1]) and 
			 ( ($XRTMode[$f + 1] != $flip_mode and $XRTMode[$f + 1] != $flip_other) 
			   or
			   ( $TIME[$f+1] - $ENDTIME[$f] > $SLEW_GAP ) 
			 ) ) );
	unless($ffend){
	  my $stable_mode = -1;
	  $stable_mode = $flip_other if $XRTMode[$f] == $flip_mode;
	  $stable_mode = $flip_mode if $XRTMode[$f] == $flip_other;
	  if($stable_mode != -1){
	    $ffend=1;
	    for(my $i=1; $i <= $FLIP_END_NUM; $i++){
	      $ffend = $ffend && (!defined($XRTMode[$f+$i]) or $XRTMode[$f+$i] == $stable_mode);
	    }
	  }
	}
	if($ffend){
#	  $log->entry("Flip-flop series ending at frame $f.");
	  $flip_flop_end[$f] = 1;
	  push(@flip_flop_series_end, $f);
	  $flip_mode = -1;
	}
      }
    }
    if($flip_mode != -1){
#     $log->entry('Flip-flop series ending at frame '. ($num_frames - 1) );
      $flip_flop_end[$num_frames - 1] = 1;
      push(@flip_flop_series_end, $num_frames - 1);      
    }

#    $log->entry('flip_flop_start = '. Dumper \@flip_flop_start);
#    $log->entry('flip_flop_series_start = '. Dumper \@flip_flop_series_start);
#    $log->entry('flip_flop_end = '. Dumper \@flip_flop_end);
#    $log->entry('flip_flop_series_end = '. Dumper \@flip_flop_series_end);

    # Make sure the starts and ends of flip-flop series match up.
    if (@flip_flop_series_start != @flip_flop_series_end) {
	$log->error(2, 'Flip-flop series starts and ends do not match up!');
	return;
    }

    # Save the number of flip-flop series.
    my($num_flip_flop_series) = scalar(@flip_flop_series_start);
    $log->entry("num_flip_flop_series = $num_flip_flop_series");

    $log->entry("The following flip-flop series were found:");
    for (my $ffs = 0; $ffs < $num_flip_flop_series; $ffs++) {
 	$log->entry("$flip_flop_series_start[$ffs]\t$flip_flop_series_end[$ffs]");
    }

    # Create an array of flags which track frames that are  part of 
    # flip-flop series, or ignored for other reasons.
    my(@use_frame) = (1) x $num_frames;

    # Mask out the flip-flop series frames
    $log->entry("Masking out flip-flop series frames.");
    for (my $ffs = 0; $ffs < $num_flip_flop_series; $ffs++) {
	for (my $f = $flip_flop_series_start[$ffs] + 1;
	     $f <= $flip_flop_series_end[$ffs]; $f++) {
	    $use_frame[$f] = 0;
	}
    }

    # Second pass: Determine the indices of the starting frames for
    # each frame set. A frame set begins when one of the following
    # conditions is met:

    # 0) The first frame.

    # 1) The XRTMode changes.

    # 2) The observation mode changes.

    # 3) The window changes.

    # 4) The bias changes.

    # 5) There is a large time gap between frames

    # 6) End of a flip-flop series

    # Note that a frame set can consist of a single frame - the start
    # and end frame indices will be equal.
    $log->entry("Determining start and end frames for frame sets.");

    # The first frame set always starts at the first frame.
    my(@frame_start) = (0);
    my(@frame_end);
    $log->entry("First frame set starts at frame 0.");

    # Find the start frame for each frame set.
    for (my $f = 1; $f < $num_frames; $f++) {

	# Skip this frame if it is ignorable.
	if (not $use_frame[$f]) {
#	    $log->entry("Skipping frame $f.");
	    next;
	}

	# Does this frame start a new frame set? If so, record it, and
	# close the previous frame set.

	if ($XRTMode[$f] != $XRTMode[$frame_start[-1]]) {
	  # XRT mode changed.
	  $log->entry('Frame set ends at '. ($f - 1) .", new set starts at $f");
	  $log->entry('Cause: XRTMode change from ' . $XRTMode[$f - 1] .
		      " to $XRTMode[$f]");
	  push(@frame_end, $f - 1);
	  push(@frame_start, $f);

	} elsif ($ObservationMode[$f] ne $ObservationMode[$frame_start[-1]]) {
	  # ObservationMode changed.
	  $log->entry('Frame set ends at '. ($f - 1) .", new set starts at $f");
	  $log->entry('Cause: ObservationMode change from ' .
		      $ObservationMode[$f - 1] . " to $ObservationMode[$f] for " .
		      "XRTMode $XRTMode[$f]");
	  push(@frame_end, $f - 1);
	  push(@frame_start, $f);

	} elsif (($XRTMode[$f] == 6 or $XRTMode[$f] == 7) and
		 $Window[$f] ne $Window[$frame_start[-1]]) {
	  # Window changed for XRT mode 6 or 7.
	  $log->entry('Frame set ends at '. ($f - 1) .", new set starts at $f");
	  $log->entry('Cause: Window change from ' . $Window[$f - 1] .
		      " to $Window[$f] for XRTMode $XRTMode[$f], ObservationMode " .
		      "$ObservationMode[$f]");
	  push(@frame_end, $f - 1);
	  push(@frame_start, $f);

	} elsif (($XRTMode[$f] == 4 or $XRTMode[$f] == 5) and
		 $bias[$f] ne $bias[$frame_start[-1]]) {
	  # Bias changed for XRT mode 4 or 5.
	  $log->entry('Frame set ends at '. ($f - 1) .", new set starts at $f");
	  $log->entry('Cause: bias change from ' . $bias[$f - 1] .
		      " to $bias[$f] for XRTMode $XRTMode[$f], ObservationMode " .
		      "$ObservationMode[$f], Window $Window[$f]");
	  push(@frame_end, $f - 1);
	  push(@frame_start, $f);

	} elsif ($TIME[$f] - $ENDTIME[$f-1] > $SLEW_GAP){
	  $log->entry('Frame set ends at '. ($f - 1) .", new set starts at $f");
	  $log->entry('Cause: time gap of ' . ($TIME[$f]-$ENDTIME[$f-1]) .
		      " for XRTMode $XRTMode[$f], ObservationMode " .
		      "$ObservationMode[$f], Window $Window[$f]");
	  push(@frame_end, $f - 1);
	  push(@frame_start, $f);
	} elsif ($flip_flop_end[$f-1]){
	  $log->entry('Frame set ends at '. ($f - 1) .", new set starts at $f");
	  $log->entry("Cause: flip_flop series end for " .
		      "XRTMode $XRTMode[$f], ObservationMode $ObservationMode[$f]");
	  push(@frame_end, $f - 1);
	  push(@frame_start, $f);
	}
    }

    # If a frame set is still open, end it on the last frame.
    if (@frame_start == @frame_end + 1) {
 	$log->entry("Ending last frame set at last frame.");
	push(@frame_end, $num_frames - 1);
    }
#     print 'frame_start = ', Dumper \@frame_start;
#     print 'frame_end = ', Dumper \@frame_end;

#    $log->entry("Frame sets:");
#    for (my $fs = 0; $fs < @frame_start; $fs++) {
#  	$log->entry("$frame_start[$fs]\t$frame_end[$fs]");
#    }

    # Make sure there are equal numbers of start and end frames.
    if (@frame_start != @frame_end) {
	$log->error(2, 'Frame set start and end frames do not match up!');
	return;
    }

    # Count the frame sets.
    my($num_frame_sets) = scalar(@frame_start);
    $log->entry("num_frame_sets = $num_frame_sets");

    #--------------------------------------------------------------------------

    # Compute the cumulative exposure time for each frame set.
    $log->entry("Computing frame set exposure times and mean temperature");
    my(@flip_flop) = ('N') x $num_frame_sets;
    my(@avg_temp) = (0) x $num_frame_sets;
    my(@exposure) = (0) x $num_frame_sets;
    for (my $fs = 0; $fs < $num_frame_sets; $fs++) {
	my($sum) = 0;
	my($tsum) = 0;
	my($n) = 0;
	my $fstart = $frame_start[$fs];
	my $flip_mode = -1;

	#####################################################
	# If this frame set contains a flip-flop,
	# need to setup to mask out the other mode
	#####################################################
	if( $flip_flop_end[$frame_end[$fs]] ){
	  my $flip_start;
	  for(my $n=0; $n < @flip_flop_series_end; $n++){
	    if($flip_flop_series_end[$n]==$frame_end[$fs]){
	      $flip_start = $flip_flop_series_start[$n];
	      last;
	    }
	  }
	  $flip_mode = $XRTMode[$flip_start+1];	  
	  $flip_flop[$fs] = 'Y';
	  ############################################
	  # Create a frame set for the flipped mode
	  ############################################
	  push @frame_start, $flip_start+1;
	  push @frame_end, $frame_end[$fs];
	  push @flip_flop, 'Y';
	  my ($flt_sum, $fl_sum, $fln) = (0, 0, 0);
	  for (my $f = $fstart; $f <= $frame_end[$fs]; $f++) {
	    next if ($XRTMode[$f] != $flip_mode);

	    my($t_start) = $TIME[$f];
	    my($t_end)   = $ENDTIME[$f];
	    $fl_sum += ($t_end - $t_start);

	    $flt_sum += $CCDTemp[$f];
	    $fln++;
	  }
	  push @exposure, $fl_sum;

	  my $t_mean = -1;
	  $t_mean = $flt_sum / $fln if $fln > 0;
	  push @avg_temp, $t_mean;	  
	}

	for (my $f = $fstart; $f <= $frame_end[$fs]; $f++) {
	  next if ($XRTMode[$f] == $flip_mode);

	  my($t_start) = $TIME[$f];
	  my($t_end)   = $ENDTIME[$f];
	  $sum += ($t_end - $t_start);

	  $tsum += $CCDTemp[$f];
	  $n++;
	}
 	$exposure[$fs] = $sum;

	my $t_mean = -1;
	$t_mean = $tsum / $n if $n > 0;
	$avg_temp[$fs] = $t_mean;
    }

#    $log->entry('exposure = '. Dumper \@exposure);
#    $log->entry('avg_temp = '. Dumper \@avg_temp);

    #--------------------------------------------------------------------------

    # Assemble an array of indices containing the start frame for each
    # frame set.
    my(@record_sets);
    for (my $fs = 0; $fs < @frame_start; $fs++) {
	push @record_sets, $fs 
	  unless $evt_path[$frame_start[$fs]] eq 'INDEF' &&
	         $XRTMode_string[$frame_start[$fs]] eq 'LOWRATE';
    }
    # Compute the final number of records.
    @record_sets = sort { $frame_start[$a] <=> $frame_start[$b] } @record_sets;
    my($num_records) = scalar(@record_sets);
    $log->entry("num_records = $num_records");
    $log->entry('record_sets = '. Dumper \@record_sets);
    
    $log->entry("Record frame sets, adjusted rows:");
    for (my $fs = 0; $fs < @record_sets; $fs++) {
  	$log->entry( ($frame_start[$record_sets[$fs]]+1) ."\t". 
		     ($frame_end[$record_sets[$fs]]+1) );
    }

    #--------------------------------------------------------------------------

    # Assemble the arrays for the database columns.

    my(@NAME) = ("'". $object ."'") x $num_records;
    $log->entry('NAME = '. Dumper \@NAME);

    my(@ORIG_TID) = (int($target)) x $num_records;
    $log->entry('ORIG_TID = '. Dumper \@ORIG_TID);

    my(@FINL_TID) = (int($target_id)) x $num_records;
    $log->entry('FINL_TID = '. Dumper \@FINL_TID);

    my(@ORIG_SEG) = (int($obs)) x $num_records;
    $log->entry('ORIG_SEG = '. Dumper \@ORIG_SEG);

    my(@FINL_SEG) = (int($segment)) x $num_records;
    $log->entry('FINL_SEG = '. Dumper \@FINL_SEG);

    my(@ORIG_SEQUENCE) = ($target . $obs) x $num_records;
    $log->entry('ORIG_SEG = '. Dumper \@ORIG_SEQUENCE);

    my(@FINL_SEQUENCE) = ($sequence) x $num_records;
    $log->entry('FINL_SEG = '. Dumper \@FINL_SEQUENCE);

    my(@RA) = (sprintf '%.5f', $ra) x $num_records;
    $log->entry('RA = '. Dumper \@RA);

    my(@DEC) = (sprintf '%.5f', $dec) x $num_records;
    $log->entry('DEC = '. Dumper \@DEC);

    my(@ROLL) = (sprintf '%.5f', $roll) x $num_records;
    $log->entry('ROLL = '. Dumper \@ROLL);

    my(@XRT_MODE) = @XRTMode_string[@frame_start[@record_sets]];
    $log->entry('XRT_MODE = '. Dumper \@XRT_MODE);

    my(@OBS_MODE) = @ObservationMode[@frame_start[@record_sets]];
    $log->entry('OBS_MODE = '. Dumper \@OBS_MODE);

    # Convert record start times to ISO date strings.
    my(@START);
    my($secs, $date, $yymmdd, $hhmmss, $datetime);
    for (my $fs = 0; $fs < $num_records; $fs++) {
      my $fstart = $frame_start[$record_sets[$fs]];
      $secs = $TIME[$fstart];
      if( $secs > 2.E09 ){
	push(@START, 'INDEF');
      }else{
	$date = Util::Date->new($secs);
	$yymmdd = $date->date;
	$hhmmss = $date->time;
	$datetime = "${yymmdd}T${hhmmss}." . int(100000*($secs-int($secs)));
	push(@START, $datetime);
      }
    }
    $log->entry('START = '. Dumper \@START);

    # Convert frame set end times to ISO date strings.
    my(@END);
    for (my $fs = 0; $fs < $num_records; $fs++) {
      my $fstop = $frame_end[$record_sets[$fs]];
      $secs = $ENDTIME[$fstop];
      if( $secs > 2.E09 ){
	push(@END, 'INDEF');
      }else{
	$date = Util::Date->new($secs);
	$yymmdd = $date->date;
	$hhmmss = $date->time;
	$datetime = "${yymmdd}T${hhmmss}." . int(100000*($secs-int($secs)));
	push(@END, $datetime);
      }
    }
    $log->entry('END = '. Dumper \@END);

    my(@BIAS) = @bias[@frame_start[@record_sets]];
    $log->entry('BIAS = '. Dumper \@BIAS);

    my(@WINDOW) = @Window[@frame_start[@record_sets]];
    $log->entry('WINDOW = '. Dumper \@WINDOW);

    my(@INT_TIME) = map {/(INDEF|NULL)/ ? $_ : sprintf('%.5f',$_)} @timedel[@frame_start[@record_sets]];
    $log->entry('INT_TIME = '. Dumper \@INT_TIME);

    my(@EXPOSURE) = map {/(INDEF|NULL)/ ? $_ : sprintf('%.5f',$_)} @exposure[@record_sets];
    $log->entry('EXPOSURE = '. Dumper \@EXPOSURE);

    my(@AVG_TEMP) = map {/(INDEF|NULL)/ ? $_ : sprintf('%.5f',$_)} @avg_temp[@record_sets];
    $log->entry('AVG_TEMP = '. Dumper \@AVG_TEMP);

    my(@EVT_FILE) = @evt_path[@frame_start[@record_sets]];
    $log->entry('EVT_FILE = '. Dumper \@EVT_FILE);

    my(@FLIP_FLOP) = @flip_flop[@record_sets];
    $log->entry('FLIP_FLOP = '. Dumper \@FLIP_FLOP);

    #--------------------------------------------------------------------------

    # Create the XRT exposure database table.

    # Insert required values into the table.
    $db->set(name           => \@NAME);
    $db->set(orig_target_id => \@ORIG_TID);
    $db->set(target_id      => \@FINL_TID);
    $db->set(orig_obs_segment => \@ORIG_SEG);
    $db->set(obs_segment    => \@FINL_SEG);
    $db->set(orig_obsid     => \@ORIG_SEQUENCE);
    $db->set(obsid          => \@FINL_SEQUENCE);
    $db->set(ra             => \@RA);
    $db->set(dec            => \@DEC);
    $db->set(roll_angle     => \@ROLL);
    $db->set(operation_mode => \@XRT_MODE);
    $db->set(pointing_mode  => \@OBS_MODE);
    $db->set(start_time     => \@START);
    $db->set(stop_time      => \@END);
    $db->set(bias           => \@BIAS);
    $db->set(window_size    => \@WINDOW);
    $db->set(integration_time => \@INT_TIME);
    $db->set(exposure       => \@EXPOSURE);
    $db->set(ccdtemp        => \@AVG_TEMP);
    $db->set(filename       => \@EVT_FILE);
    $db->set(flipflop       => \@FLIP_FLOP);

    # Compute the name of the XRT exposure database file.
    my($outfile) = $filename->get('xrdb', 'proc', '', 0);
    # $log->entry("outfile = $outfile\n");

    # Create the XRT exposure database file.
    $db->write($outfile);

    # Print out accumulated warnings, after removing duplicates
    my %messages;
    @messages{@warnings} = (1) x @warnings;
    foreach (keys %messages){
      $log->error(1, $_);
    }

    $log->entry("Ending XRTDB::body().");

}

1;