Index: /branches/ccl_branches/ipponly-20191108/pstamp/Makefile.am
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/Makefile.am	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/Makefile.am	(revision 41103)
@@ -0,0 +1,9 @@
+SUBDIRS = src scripts
+
+pkgconfigdir = $(libdir)/pkgconfig
+pkgconfig_DATA= ppstamp.pc
+
+EXTRA_DIST = \
+	ppstamp.pc.in
+
+CLEANFILES = *~ core core.*
Index: /branches/ccl_branches/ipponly-20191108/pstamp/autogen.sh
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/autogen.sh	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/autogen.sh	(revision 41103)
@@ -0,0 +1,103 @@
+#!/bin/sh
+# Run this to generate all the initial makefiles, etc.
+
+srcdir=`dirname $0`
+test -z "$srcdir" && srcdir=.
+
+ORIGDIR=`pwd`
+cd $srcdir
+
+PROJECT=pstamp
+TEST_TYPE=-f
+# change this to be a unique filename in the top level dir
+FILE=autogen.sh
+
+DIE=0
+
+LIBTOOLIZE=libtoolize
+ACLOCAL="aclocal $ACLOCAL_FLAGS"
+AUTOHEADER=autoheader
+AUTOMAKE=automake
+AUTOCONF=autoconf
+
+($LIBTOOLIZE --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $LIBTOOLIZE installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/libtool/"
+        DIE=1
+}
+
+($ACLOCAL --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $ACLOCAL installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+($AUTOHEADER --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOHEADER installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+        DIE=1
+}
+
+($AUTOMAKE --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOMAKE installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+($AUTOCONF --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOCONF installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+        DIE=1
+}
+
+if test "$DIE" -eq 1; then
+        exit 1
+fi
+
+test $TEST_TYPE $FILE || {
+        echo "You must run this script in the top-level $PROJECT directory"
+        exit 1
+}
+
+if test -z "$*"; then
+        echo "I am going to run ./configure with no arguments - if you wish "
+        echo "to pass any to it, please specify them on the $0 command line."
+fi
+
+$LIBTOOLIZE --copy --force || echo "$LIBTOOLIZE failed"
+$ACLOCAL || echo "$ACLOCAL failed"
+$AUTOHEADER || echo "$AUTOHEADER failed"
+$AUTOMAKE --add-missing --force-missing --copy || echo "$AUTOMAKE failed"
+$AUTOCONF || echo "$AUTOCONF failed"
+
+cd $ORIGDIR
+
+run_configure=true
+for arg in $*; do
+    case $arg in
+        --no-configure)
+            run_configure=false
+            ;;
+        *)
+            ;;
+    esac
+done
+
+if $run_configure; then
+    $srcdir/configure --enable-maintainer-mode "$@"
+    echo
+    echo "Now type 'make' to compile $PROJECT."
+else
+    echo
+    echo "Now run 'configure' and 'make' to compile $PROJECT."
+fi
Index: /branches/ccl_branches/ipponly-20191108/pstamp/configure.ac
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/configure.ac	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/configure.ac	(revision 41103)
@@ -0,0 +1,65 @@
+dnl Process this file with autoconf to produce a configure script.
+AC_PREREQ(2.61)
+
+AC_INIT([ppstamp], [0.1.1], [ipp-support@ifa.hawaii.edu])
+AC_CONFIG_SRCDIR([src])
+AC_CONFIG_SRCDIR([scripts])
+
+AM_INIT_AUTOMAKE([1.6 foreign dist-bzip2])
+AM_CONFIG_HEADER([src/config.h])
+AM_MAINTAINER_MODE
+
+IPP_STDLDFLAGS
+
+AC_LANG(C)
+AC_GNU_SOURCE
+AC_PROG_CC_C99
+AC_PROG_INSTALL
+AC_PROG_LIBTOOL
+AC_SYS_LARGEFILE
+
+dnl ------------------------------------------------------------
+
+AC_PATH_PROG([ERRORCODES], [psParseErrorCodes], [missing])
+if test "$ERRORCODES" = "missing" ; then
+  AC_MSG_ERROR([psParseErrorCodes is required])
+fi
+
+
+PKG_CHECK_MODULES([PSLIB], [pslib >= 1.0.0])
+PKG_CHECK_MODULES([PSMODULE], [psmodules >= 1.0.0])
+
+OHANA_CONFIG=`which ohana-config`
+
+dnl get CFLAGS for ohana
+AC_MSG_NOTICE([libohana cflags info supplied by ohana-config])
+AC_MSG_CHECKING([libohana cflags])
+OHANA_CFLAGS="`${OHANA_CONFIG} --cflags`"
+AC_MSG_RESULT([${OHANA_CFLAGS}])
+
+dnl get LDFLAGS for ohana
+AC_MSG_NOTICE([libohana ldflags info supplied by ohana-config])
+AC_MSG_CHECKING([libohana ldflags])
+OHANA_LIBS="`${OHANA_CONFIG} --libs`"
+AC_MSG_RESULT([${OHANA_LIBS}])
+
+dnl Set CFLAGS for build
+IPP_STDOPTS
+IPP_STDCFLAGS
+
+PPSTAMP_CFLAGS="${PSLIB_CFLAGS=} ${PSMODULE_CFLAGS=} ${OHANA_CFLAGS}"
+PPSTAMP_LIBS="${PSLIB_LIBS=} ${PSMODULE_LIBS=} ${OHANA_LIBS}"
+echo "PPSTAMP_CFLAGS: $PPSTAMP_CFLAGS"
+echo "PPSTAMP_LIBS: $PPSTAMP_LIBS"
+
+AC_SUBST([PPSTAMP_CFLAGS])
+AC_SUBST([PPSTAMP_LIBS])
+
+AC_CONFIG_FILES([
+  Makefile
+  src/Makefile
+  scripts/Makefile
+  ppstamp.pc
+])
+
+AC_OUTPUT
Index: /branches/ccl_branches/ipponly-20191108/pstamp/doc/dsinstall.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/doc/dsinstall.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/doc/dsinstall.txt	(revision 41103)
@@ -0,0 +1,417 @@
+Installing the data store sample implementation
+XXX: This text was found lying around in my workspace. It needs to be
+checked for accuarcy
+
+Overview
+--------
+
+This note describes the installation and operation of the IPP Postage Stamp Server.
+We assume that IPP source for examination and that the user has the standard
+IPP configuration environment set up.
+
+This component of the IPP system is not intended for general distribution to IPP users
+and does not have streamlined installation procedures at this time.
+
+The PSS is a system which responds to requests to retrieve images from an IPP Image Server.
+The images may be either complete images or subsets of images known as "Postage Stamps".
+
+The internal mechanisms of the PSS are also used to service MOPS detectabiltiy
+query requests.
+
+Given this more general utility it should perhaps be called the "IPP Request Server".
+
+
+Data Store
+----------
+The PSS uses the Data Store mechanism to find incoming requests and to 
+provide results to the end user.
+
+A DataStore is simply a web server with URLS that supports an interface
+that allows collections of data called File Sets to be located and accessed.
+
+A Data Store is organized into 3 levels. These are described in the following sections.
+
+Data Store Root - a list of Products
+-------------------------------------
+
+Assume that a datastore is located on the server WEBSERVERHOST.
+An HTTP get to the url http://WEBSERVERHOST/ds/index.txt would give a list of the
+products in the Data Store.
+
+    # productID  |Most recent |Time registered     |type     |Description                     |
+    pstampresults|web204      |2008-10-16T00:35:16Z|psresults|Postage Stamp Results           |
+    pstamprequests|TEST0012    |2008-08-29T03:26:40Z|psrequest|Postage Stamp Requests          |
+    mops-pstamp-results|MOPSSTAMPREQ20080813T214918Z_00|2008-08-13T21:49:42Z|psresults|Postage Stamp Results for MOPS  |
+
+Each line is divided into fields separated by a 'pipe' character | .
+
+Data Store Product - a list of filesets
+---------------------------------------
+
+Similarly the url http://WEBSERVERHOST/ds/pstampresults/index.txt gives the list of filesets
+in the product pstampresults:
+
+    # filesetID|time registered     |type     |
+    TEST0011   |2008-08-29T03:05:03Z|PSRESULTS|
+    TEST0012   |2008-08-29T03:31:36Z|PSRESULTS|
+    web190     |2008-10-15T02:29:11Z|PSRESULTS|
+    web191     |2008-10-15T02:32:16Z|PSRESULTS|
+    web192     |2008-10-15T02:38:50Z|PSRESULTS|
+    web193     |2008-10-15T03:08:57Z|PSRESULTS|
+    web194     |2008-10-15T23:32:11Z|PSRESULTS|
+    web195     |2008-10-15T23:35:39Z|PSRESULTS|
+    web196     |2008-10-15T23:52:40Z|PSRESULTS|
+    web197     |2008-10-15T23:59:05Z|PSRESULTS|
+    web198     |2008-10-16T00:04:17Z|PSRESULTS|
+    web200     |2008-10-16T00:17:46Z|PSRESULTS|
+    web204     |2008-10-16T00:35:16Z|PSRESULTS|
+
+Note that the fileset ID for the latest fileset (web204) in the list appears in the root listing in the last
+section.
+
+The product list protocol accepts a query string which can be used to limit the listing to all filesets
+registered after a given fileset.
+
+For example, http://WEBSERVERHOST/ds/pstampresults/index.txt?web198 yields
+
+    # filesetID|time registered     |type     |
+    web200     |2008-10-16T00:17:46Z|PSRESULTS|
+    web204     |2008-10-16T00:35:16Z|PSRESULTS|
+
+
+File Set - a list of files
+--------------------------
+http://ippds.somwhere.net/ds/pstampresults/web205/index.txt
+
+    # fileID          |bytes   |md5sum                          |type        |
+    results.fits      |28800   |4e2048479e551844e31102a301d50740|table       |
+    # fileID          |bytes   |md5sum                          |type        |chipname|
+    1_1_o4748g0259o.ota01.fits|19840320|19a6233ab966cf3e13b7da8271dead67|chip        |XY01    |
+    1_2_o4748g0259o.ota02.fits|20416320|625bf8198dd7aef7c892af03584ee0ed|chip        |XY02    |
+
+    .... followed by 58 other files in this GPC1 exposure
+
+
+The IPP includes a number of tools for accessing a data store. An introduction to these tools may
+be seen by using the program perldoc.
+
+    dsrootls
+    dsproductls
+    dsfilesetls
+    dsget
+
+
+Installing the IPP Data Store Server Implementation
+---------------------------------------------------
+
+The data store implementation consists of a set of cgi scripts which are
+invoked by an apache web server. The indexes for the Data Store are stored in a mysql
+database. The cgi scripts use DBI perl module to query the database. No database updates
+are performed by the Data Store cgi scripts.
+
+The files for the Data Store Server located in the IPP source tree in the directory
+DataStoreServer (this directory may not be available in the distributed IPP
+tarballs).
+
+Mysql data base configuration
+-----------------------------
+
+The tables for the PSS are part of the regular IPP database tables.
+
+The Data Store implementation has been designed to not require the IPP for its
+operation so the Data Store's tables are not created as part of the IPP database setup.
+
+Currently however, the PostageStamp Server implementation assumes that the data
+store and postage stamp tables are in the same mysql database (this will be
+fixed soon).
+
+To set up the database go to the directory DataStoreServer/scripts and start up mysql.
+
+If you do not already have a current IPP database that you would like to use
+for the Postage Stamp Request tables issue the following commands
+
+    mysql> create database mydatabase;
+    mysql> use mydatabase;
+    mysql> source scripts/tabledefs.sql;
+
+Now exit mysql and run the command
+    pxadmin -create -dbname mydatabase
+
+If you already have a database set up, then just issue the commands
+    mysql> use mydatabase;
+    mysql> source scripts/tabledefs.sql;
+
+Web Server Configuration
+------------------------
+The apache configuration needs to have the following entries added to
+the appropriate configuration file. (Different apache distributions have
+different locations for these files so it's up to the installer to
+figure out where these configuration lines should go).
+
+       <Directory "/DATASTOREDIR/dsroot">
+
+          DirectoryIndex /ds-cgi/dsindex.cgi
+
+          #Options None
+
+          # turn on server side includes
+          Options +Includes
+
+          # files will be processed for server side includes if they have the
+          # executable bit set
+          XBitHack on
+
+          AddType text/plain .txt
+          AddOutputFilter INCLUDES .txt
+
+          AllowOverride None
+
+          Order allow,deny
+          Allow from all
+        </Directory>
+
+        #
+        # Location of the cgi scripts
+        #
+
+        <Directory "/DATASTOREDIR/ds-cgi">
+          Options ExecCGI
+
+
+          Order allow,deny
+          Allow from all
+        </Directory>
+
+        Alias       /ds     /DATASTOREDIR/dsroot/
+        ScriptAlias /ds-cgi /DATASTOREDIR/ds-cgi/
+
+(another sample of this configuration file may be found in 
+    DataStoreServer/web/conf/httpd-datastore.conf
+
+Here we have assumed that the data store cgi scripts will be installed in
+/DATASTOREDIR/ds-cgi and the data store data files will be stored in
+/DATASTOREDIR/dsroot.
+
+To install the cgi scripts
+    cd your_ipp_src_directory
+    cd DataStoreServer
+    scripts/installscripts --dsroot /DATASTOREDIR/dsroot --cgidir /DATASTOREDIR/ds-cgi
+
+The file /DATASTOREDIR/ds-cgi/dsshellconfig is a shell script that sets
+several environment variables that are required by the scripts.
+
+This script should be editied to customize the values for the installation.
+(This file will not be overwritten by subsequent invocations of
+installscripts).
+
+There are some scripts that the Postage Stamp Server uses that need to be
+installed into the IPP build directory. These are not included in the current IPP
+build. To build them invoke the following commands from the DataStoreServer
+directory.
+    psautogen
+    make install
+
+
+Testing the DataStore
+---------------------
+At this point an empty data store should be up and running. This can be tested
+by going to the following url in a web browser
+    http://WEBSERVERHOST/ds/index.txt
+
+If things are working properlly you should see the following output
+
+# productID  |Most recent |Time registered     |type     |Description                     |
+
+If an errror occurs check the web server error log and begin debugging.
+
+The following command may be used to create a test product in the data store
+
+dsprodtool --dbname mydatabase --dsroot   /DATASTOREDIR/dsroot/ \
+     --add testproduct --type psresults --description 'test data store product'
+
+Now if you point to http://WEBSERVERHOST/ds/index.txt with a web browser you
+should see 
+
+# productID  |Most recent |Time registered     |type     |Description                     |
+testproduct  |none        |2008-10-16T20:48:23Z|psresults|test data store product         |
+
+(of course your timestamp will be different).
+
+As a final test we will add a fileset to the data store.
+
+echo this is a file > testfile.txt
+
+echo 'testfile.txt|||text|' | dsreg --dbname mydatabase --dsroot /DATASTOREDIR/dsroot/ \
+    --product testproduct --add testfileset --type PSRESULTS --list - --copy --datapath .
+
+-list - tells dsreg to get the list of files for the fileset from stdin.
+(Usually a file is used).  The input to the command gives the name of the file to be added to the
+fileset, blank entries for size and md5sum, and the file type 'text'. 
+
+Now if you go to http://WEBSERVERHOST/ds/testfileset/index.txt you should see
+the fileset.
+
+If you leave off the index.txt in the urls you will get a more web browser
+friendly html pages with links.
+For example
+    http://WEBSERVERHOST/ds
+
+To delete the test product
+    dsprodtool --dbname mydatabase --dsroot /DATASTOREDIR/dsroot/ --del psresults --rm
+
+Set up the product for the postage stamp results with
+
+    dsprodtool --dbname mydatabase --dsroot /DATASTOREDIR/dsroot/ \
+         --add pstampresults --type psresults --description 'Postage Stamp results'
+
+and a product for requests
+
+    dsprodtool --dbname mydatabase --dsroot /DATASTOREDIR/dsroot/ \
+         --add pstamprequests --type psrequest --description 'Postage Stamp requests'
+
+Postage Stamp Server Set up
+---------------------------
+
+Now we are ready to set up the Postage Stamp Server.
+
+The programs that make up the PSS are controlled by a set of pantasks tasks. 
+There are some configuration variables that must be added to the site.config file.
+
+    # root directory of the data store
+    # this must match the directory used above in the Data Store setup
+    DATA_STORE_ROOT STR /DATASTOREDIR/dsroot
+
+    # default data store product used for results 
+    # (used by the web interface)
+    PSTAMP_DATA_STORE_PRODUCT STR pstampresults
+
+    # This directory is used for downloaded request files and other purposes
+    # it can go anywhere. The permissions on the directory must be such that
+    # that the web server program has permission to write there.
+    PSTAMP_WORKDIR STR /SOMEWHERE/pstamp-work
+
+PSS projects
+------------
+Each Postage stamp request selects images from a particular 'project'. 
+Each project has a row in the table pstampProject has the following columns
+
+    column      example
+    -------------------
+    name        simtest
+    state       enabled
+    dbname      ps_simtest
+    dvodb       NULL      (currently not used)
+    camera      SIMTEST
+    telescope   SimScope
+    need_magic  0
+
+The column dbname contains the name of the database that is used to look up images.
+To create a project use pstamptool. In this example ps_simtest is a database
+refererencing images made by a simtest run.
+
+pstamptool -dbname mydatabase -addproject -name foo -imagedb ps_simtest -inst SIMTEST -telescope SimScope
+
+
+Postage Stamp Request Tables
+----------------------------
+
+Postage Stamp requests are submitted in the form of a FITS binary table. The
+format of this table is described in a document which may be found on the IPP wiki.
+http://kiawe.ifa.hawaii.edu/IPPwiki/index.php/PostageStampServer
+
+Only the first table extension found in the request file is processed. 
+
+Each postage stamp request file submitted to the PSS generates a "Request".
+The name of the request is given by the value for the keyword REQ_NAME in
+the extension's fits header. Each postage stamp request must have a unique REQ_NAME must be unique.
+
+To process the request the PSS parses the request file.
+Each row in a request table contains a request specification. Each request specification
+causes zero or more "Jobs" to be queued for processing. Each Job operates on
+one input image.
+
+When all of the Jobs for a given Request complete, a Postage stamp results
+file is created and it along with the images produced by the jobs are
+registered in a fileset on the output Data Store.
+
+There are two job types specified by the value for the JOB_TYPE keyword in the
+request specification.
+
+    JOB_TYPE = "stamp" creates a "postage stamp" image.
+    JOB_TYPE = "get_image" builds a fileset containing copies of images
+
+There are two programs that may be used to create a postage stamp request.
+pstamprequest creates a request file with a single request specification.
+
+For example
+
+    pstamprequest -req_name REQ_00001 -project simtest /mydirectory/req.fits \
+                  -byexp chip simtest.004.000 null \
+                  -pixcenter 500 500 -pixrange 100 100
+
+creates a request file req.fits containing a request specification for a 100 x 100 pixel 
+postage stamp image centered at (500,500) in the source.
+The source image is chip processed image for one of the object exposures from a simtest run.
+
+Another program that may be used to create a request file is located in
+pstamp/test/pstamp_req_create.  This program takes a text file and creates a multi-row postage
+stamp request.  A couple of example input files may be found in pstamp/test/*.txt
+
+Queueing a PostageStamp Request
+-------------------------------
+The postage stamp server looks for work to do by examining a collection of
+input Data Stores. For example, the MOPS team will have a datastore onto which
+they will submit their postage stamp requests.
+
+There will also be a web based interface. A prototype for this will be
+discussed in a later section of this document.
+
+For testing, we may queue a request by hand using the program pstamptool
+
+    pstamptool -dbname mydatabase -addreq -uri /mydirectory/req.fits
+
+This adds an entry to the table pstampRequests with state = 'new'
+
+Request Processing
+------------------
+
+Postage stamp requests are processed by a set of pantasks tasks.
+
+ Task Status
+  AV Name                Njobs  Ngood Nfail Ntime Command            
+  ++ pstamp.request.find   921    921     0     0 pstamp_queue_requests.pl
+  ++ pstamp.request.run     13     13     0     0 pstamp_parser_run.pl
+  ++ request.finish.load  1839   1839     0     0 pstamptool         
+  ++ request.finish.run     14     14     0     0 request_finish.pl  
+  ++ pstamp.job.load      1840   1840     0     0 pstamptool         
+  ++ pstamp.job.run         30     30     0     0 pstamp_job_run.pl  
+  ++ pstamp.request.load  1566   1566     0     0 pstamptool
+
+pstamp.request.find examines incoming data stores for new requests and adds
+them if found
+
+pstamp.request.load dispatches pending requests for parsing
+
+pstamp.request.run runs the request parser which queues jobs to service the
+request specifications.
+
+pstamp.job.load dispatches pending jobs for processing
+
+pstamp.job.run runs a single job
+
+pstamp.finish.load looks for requests which have no pending jobs
+
+pstamp.finish.run builds the results fileset and registers it on the ouptut
+Data Store.
+
+Output Fileset
+-------------
+A successful request will generate a fileset with the name the REQ_NAME of the
+request in the output Data Store. The output product is determined by the
+source of the request as discussed in the next section. (If the request did
+not come from a data store the product is given by the value of PSTAMP_DATA_STORE_PRODUCT
+in the IPP site.config file.
+
+
+
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/doc/psnotes.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/doc/psnotes.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/doc/psnotes.txt	(revision 41103)
@@ -0,0 +1,415 @@
+Notes on the IPP Postage Stamp Server
+
+Overview
+--------
+
+This note describes the installation and operation of the IPP Postage Stamp Server.
+We assume that IPP source for examination and that the user has the standard
+IPP configuration environment set up.
+
+This component of the IPP system is not intended for general distribution to IPP users
+and does not have streamlined installation procedures at this time.
+
+The PSS is a system which responds to requests to retrieve images from an IPP Image Server.
+The images may be either complete images or subsets of images known as "Postage Stamps".
+
+The internal mechanisms of the PSS are also used to service MOPS detectabiltiy
+query requests.
+
+Given this more general utility it should perhaps be called the "IPP Request Server".
+
+
+Data Store
+----------
+The PSS uses the Data Store mechanism to find incoming requests and to 
+provide results to the end user.
+
+A DataStore is simply a web server with URLS that supports an interface
+that allows collections of data called File Sets to be located and accessed.
+
+A Data Store is organized into 3 levels. These are described in the following sections.
+
+Data Store Root - a list of Products
+-------------------------------------
+
+Assume that a datastore is located on the server WEBSERVERHOST.
+An HTTP get to the url http://WEBSERVERHOST/ds/index.txt would give a list of the
+products in the Data Store.
+
+    # productID  |Most recent |Time registered     |type     |Description                     |
+    pstampresults|web204      |2008-10-16T00:35:16Z|psresults|Postage Stamp Results           |
+    pstamprequests|TEST0012    |2008-08-29T03:26:40Z|psrequest|Postage Stamp Requests          |
+    mops-pstamp-results|MOPSSTAMPREQ20080813T214918Z_00|2008-08-13T21:49:42Z|psresults|Postage Stamp Results for MOPS  |
+
+Each line is divided into fields separated by a 'pipe' character | .
+
+Data Store Product - a list of filesets
+---------------------------------------
+
+Similarly the url http://WEBSERVERHOST/ds/pstampresults/index.txt gives the list of filesets
+in the product pstampresults:
+
+    # filesetID|time registered     |type     |
+    TEST0011   |2008-08-29T03:05:03Z|PSRESULTS|
+    TEST0012   |2008-08-29T03:31:36Z|PSRESULTS|
+    web190     |2008-10-15T02:29:11Z|PSRESULTS|
+    web191     |2008-10-15T02:32:16Z|PSRESULTS|
+    web192     |2008-10-15T02:38:50Z|PSRESULTS|
+    web193     |2008-10-15T03:08:57Z|PSRESULTS|
+    web194     |2008-10-15T23:32:11Z|PSRESULTS|
+    web195     |2008-10-15T23:35:39Z|PSRESULTS|
+    web196     |2008-10-15T23:52:40Z|PSRESULTS|
+    web197     |2008-10-15T23:59:05Z|PSRESULTS|
+    web198     |2008-10-16T00:04:17Z|PSRESULTS|
+    web200     |2008-10-16T00:17:46Z|PSRESULTS|
+    web204     |2008-10-16T00:35:16Z|PSRESULTS|
+
+Note that the fileset ID for the latest fileset (web204) in the list appears in the root listing in the last
+section.
+
+The product list protocol accepts a query string which can be used to limit the listing to all filesets
+registered after a given fileset.
+
+For example, http://WEBSERVERHOST/ds/pstampresults/index.txt?web198 yields
+
+    # filesetID|time registered     |type     |
+    web200     |2008-10-16T00:17:46Z|PSRESULTS|
+    web204     |2008-10-16T00:35:16Z|PSRESULTS|
+
+
+File Set - a list of files
+--------------------------
+http://ippds.somwhere.net/ds/pstampresults/web205/index.txt
+
+    # fileID          |bytes   |md5sum                          |type        |
+    results.fits      |28800   |4e2048479e551844e31102a301d50740|table       |
+    # fileID          |bytes   |md5sum                          |type        |chipname|
+    1_1_o4748g0259o.ota01.fits|19840320|19a6233ab966cf3e13b7da8271dead67|chip        |XY01    |
+    1_2_o4748g0259o.ota02.fits|20416320|625bf8198dd7aef7c892af03584ee0ed|chip        |XY02    |
+
+    .... followed by 58 other files in this GPC1 exposure
+
+
+The IPP includes a number of tools for accessing a data store. An introduction to these tools may
+be seen by using the program perldoc.
+
+    dsrootls
+    dsproductls
+    dsfilesetls
+    dsget
+
+
+Installing the IPP Data Store Server Implementation
+---------------------------------------------------
+
+The data store implementation consists of a set of cgi scripts which are
+invoked by an apache web server. The indexes for the Data Store are stored in a mysql
+database. The cgi scripts use DBI perl module to query the database. No database updates
+are performed by the Data Store cgi scripts.
+
+The files for the Data Store Server located in the IPP source tree in the directory
+DataStoreServer (this directory may not be available in the distributed IPP
+tarballs).
+
+Mysql data base configuration
+-----------------------------
+
+The tables for the PSS are part of the regular IPP database tables.
+
+The Data Store implementation has been designed to not require the IPP for its
+operation so the Data Store's tables are not created as part of the IPP database setup.
+
+Currently however, the PostageStamp Server implementation assumes that the data
+store and postage stamp tables are in the same mysql database (this will be
+fixed soon).
+
+To set up the database go to the directory DataStoreServer/scripts and start up mysql.
+
+If you do not already have a current IPP database that you would like to use
+for the Postage Stamp Request tables issue the following commands
+
+    mysql> create database mydatabase;
+    mysql> use mydatabase;
+    mysql> source scripts/tabledefs.sql;
+
+Now exit mysql and run the command
+    pxadmin -create -dbname mydatabase
+
+If you already have a database set up, then just issue the commands
+    mysql> use mydatabase;
+    mysql> source scripts/tabledefs.sql;
+
+Web Server Configuration
+------------------------
+The apache configuration needs to have the following entries added to
+the appropriate configuration file. (Different apache distributions have
+different locations for these files so it's up to the installer to
+figure out where these configuration lines should go).
+
+       <Directory "/DATASTOREDIR/dsroot">
+
+          DirectoryIndex /ds-cgi/dsindex.cgi
+
+          #Options None
+
+          # turn on server side includes
+          Options +Includes
+
+          # files will be processed for server side includes if they have the
+          # executable bit set
+          XBitHack on
+
+          AddType text/plain .txt
+          AddOutputFilter INCLUDES .txt
+
+          AllowOverride None
+
+          Order allow,deny
+          Allow from all
+        </Directory>
+
+        #
+        # Location of the cgi scripts
+        #
+
+        <Directory "/DATASTOREDIR/ds-cgi">
+          Options ExecCGI
+
+
+          Order allow,deny
+          Allow from all
+        </Directory>
+
+        Alias       /ds     /DATASTOREDIR/dsroot/
+        ScriptAlias /ds-cgi /DATASTOREDIR/ds-cgi/
+
+(another sample of this configuration file may be found in 
+    DataStoreServer/web/conf/httpd-datastore.conf
+
+Here we have assumed that the data store cgi scripts will be installed in
+/DATASTOREDIR/ds-cgi and the data store data files will be stored in
+/DATASTOREDIR/dsroot.
+
+To install the cgi scripts
+    cd your_ipp_src_directory
+    cd DataStoreServer
+    scripts/installscripts --dsroot /DATASTOREDIR/dsroot --cgidir /DATASTOREDIR/ds-cgi
+
+The file /DATASTOREDIR/ds-cgi/dsshellconfig is a shell script that sets
+several environment variables that are required by the scripts.
+
+This script should be editied to customize the values for the installation.
+(This file will not be overwritten by subsequent invocations of
+installscripts).
+
+There are some scripts that the Postage Stamp Server uses that need to be
+installed into the IPP build directory. These are not included in the current IPP
+build. To build them invoke the following commands from the DataStoreServer
+directory.
+    psautogen
+    make install
+
+
+Testing the DataStore
+---------------------
+At this point an empty data store should be up and running. This can be tested
+by going to the following url in a web browser
+    http://WEBSERVERHOST/ds/index.txt
+
+If things are working properlly you should see the following output
+
+# productID  |Most recent |Time registered     |type     |Description                     |
+
+If an errror occurs check the web server error log and begin debugging.
+
+The following command may be used to create a test product in the data store
+
+dsprodtool --dbname mydatabase --dsroot   /DATASTOREDIR/dsroot/ \
+     --add testproduct --type psresults --description 'test data store product'
+
+Now if you point to http://WEBSERVERHOST/ds/index.txt with a web browser you
+should see 
+
+# productID  |Most recent |Time registered     |type     |Description                     |
+testproduct  |none        |2008-10-16T20:48:23Z|psresults|test data store product         |
+
+(of course your timestamp will be different).
+
+As a final test we will add a fileset to the data store.
+
+echo this is a file > testfile.txt
+
+echo 'testfile.txt|||text|' | dsreg --dbname mydatabase --dsroot /DATASTOREDIR/dsroot/ \
+    --product testproduct --add testfileset --type PSRESULTS --list - --copy --datapath .
+
+-list - tells dsreg to get the list of files for the fileset from stdin.
+(Usually a file is used).  The input to the command gives the name of the file to be added to the
+fileset, blank entries for size and md5sum, and the file type 'text'. 
+
+Now if you go to http://WEBSERVERHOST/ds/testfileset/index.txt you should see
+the fileset.
+
+If you leave off the index.txt in the urls you will get a more web browser
+friendly html pages with links.
+For example
+    http://WEBSERVERHOST/ds
+
+To delete the test product
+    dsprodtool --dbname mydatabase --dsroot /DATASTOREDIR/dsroot/ --del psresults --rm
+
+Set up the product for the postage stamp results with
+
+    dsprodtool --dbname mydatabase --dsroot /DATASTOREDIR/dsroot/ \
+         --add pstampresults --type psresults --description 'Postage Stamp results'
+
+and a product for requests
+
+    dsprodtool --dbname mydatabase --dsroot /DATASTOREDIR/dsroot/ \
+         --add pstamprequests --type psrequest --description 'Postage Stamp requests'
+
+Postage Stamp Server Set up
+---------------------------
+
+Now we are ready to set up the Postage Stamp Server.
+
+The programs that make up the PSS are controlled by a set of pantasks tasks. 
+There are some configuration variables that must be added to the site.config file.
+
+    # root directory of the data store
+    # this must match the directory used above in the Data Store setup
+    DATA_STORE_ROOT STR /DATASTOREDIR/dsroot
+
+    # default data store product used for results 
+    # (used by the web interface)
+    PSTAMP_DATA_STORE_PRODUCT STR pstampresults
+
+    # This directory is used for downloaded request files and other purposes
+    # it can go anywhere. The permissions on the directory must be such that
+    # that the web server program has permission to write there.
+    PSTAMP_WORKDIR STR /SOMEWHERE/pstamp-work
+
+PSS projects
+------------
+Each Postage stamp request selects images from a particular 'project'. 
+Each project has a row in the table pstampProject has the following columns
+
+    column      example
+    -------------------
+    name        simtest
+    state       enabled
+    dbname      ps_simtest
+    dvodb       NULL      (currently not used)
+    camera      SIMTEST
+    telescope   SimScope
+    need_magic  0
+
+The column dbname contains the name of the database that is used to look up images.
+To create a project use pstamptool. In this example ps_simtest is a database
+refererencing images made by a simtest run.
+
+pstamptool -dbname mydatabase -addproject -name foo -imagedb ps_simtest -inst SIMTEST -telescope SimScope
+
+
+Postage Stamp Request Tables
+----------------------------
+
+Postage Stamp requests are submitted in the form of a FITS binary table. The
+format of this table is described in a document which may be found on the IPP wiki.
+http://kiawe.ifa.hawaii.edu/IPPwiki/index.php/PostageStampServer
+
+Only the first table extension found in the request file is processed. 
+
+Each postage stamp request file submitted to the PSS generates a "Request".
+The name of the request is given by the value for the keyword REQ_NAME in
+the extension's fits header. Each postage stamp request must have a unique REQ_NAME must be unique.
+
+To process the request the PSS parses the request file.
+Each row in a request table contains a request specification. Each request specification
+causes zero or more "Jobs" to be queued for processing. Each Job operates on
+one input image.
+
+When all of the Jobs for a given Request complete, a Postage stamp results
+file is created and it along with the images produced by the jobs are
+registered in a fileset on the output Data Store.
+
+There are two job types specified by the value for the JOB_TYPE keyword in the
+request specification.
+
+    JOB_TYPE = "stamp" creates a "postage stamp" image.
+    JOB_TYPE = "get_image" builds a fileset containing copies of images
+
+There are two programs that may be used to create a postage stamp request.
+pstamprequest creates a request file with a single request specification.
+
+For example
+
+    pstamprequest -req_name REQ_00001 -project simtest /mydirectory/req.fits \
+                  -byexp chip simtest.004.000 null \
+                  -pixcenter 500 500 -pixrange 100 100
+
+creates a request file req.fits containing a request specification for a 100 x 100 pixel 
+postage stamp image centered at (500,500) in the source.
+The source image is chip processed image for one of the object exposures from a simtest run.
+
+Another program that may be used to create a request file is located in
+pstamp/test/pstamp_req_create.  This program takes a text file and creates a multi-row postage
+stamp request.  A couple of example input files may be found in pstamp/test/*.txt
+
+Queueing a PostageStamp Request
+-------------------------------
+The postage stamp server looks for work to do by examining a collection of
+input Data Stores. For example, the MOPS team will have a datastore onto which
+they will submit their postage stamp requests.
+
+There will also be a web based interface. A prototype for this will be
+discussed in a later section of this document.
+
+For testing, we may queue a request by hand using the program pstamptool
+
+    pstamptool -dbname mydatabase -addreq -uri /mydirectory/req.fits
+
+This adds an entry to the table pstampRequests with state = 'new'
+
+Request Processing
+------------------
+
+Postage stamp requests are processed by a set of pantasks tasks.
+
+ Task Status
+  AV Name                Njobs  Ngood Nfail Ntime Command            
+  ++ pstamp.request.find   921    921     0     0 pstamp_queue_requests.pl
+  ++ pstamp.request.run     13     13     0     0 pstamp_parser_run.pl
+  ++ request.finish.load  1839   1839     0     0 pstamptool         
+  ++ request.finish.run     14     14     0     0 request_finish.pl  
+  ++ pstamp.job.load      1840   1840     0     0 pstamptool         
+  ++ pstamp.job.run         30     30     0     0 pstamp_job_run.pl  
+  ++ pstamp.request.load  1566   1566     0     0 pstamptool
+
+pstamp.request.find examines incoming data stores for new requests and adds
+them if found
+
+pstamp.request.load dispatches pending requests for parsing
+
+pstamp.request.run runs the request parser which queues jobs to service the
+request specifications.
+
+pstamp.job.load dispatches pending jobs for processing
+
+pstamp.job.run runs a single job
+
+pstamp.finish.load looks for requests which have no pending jobs
+
+pstamp.finish.run builds the results fileset and registers it on the ouptut
+Data Store.
+
+Output Fileset
+-------------
+A successful request will generate a fileset with the name the REQ_NAME of the
+request in the output Data Store. The output product is determined by the
+source of the request as discussed in the next section. (If the request did
+not come from a data store the product is given by the value of PSTAMP_DATA_STORE_PRODUCT
+in the IPP site.config file.
+
+
+
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/ppstamp.pc.in
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/ppstamp.pc.in	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/ppstamp.pc.in	(revision 41103)
@@ -0,0 +1,11 @@
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@/@PACKAGE_NAME@
+
+Name: @PACKAGE_NAME@
+Description: Pan-STARRS Postage Stamper
+Version: @VERSION@
+Requires: pslib psmodules
+Libs: -L${libdir} @PPSTAMP_LIBS@
+Cflags: -I${includedir} @PPSTAMP_CFLAGS@
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/Makefile.am
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/Makefile.am	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/Makefile.am	(revision 41103)
@@ -0,0 +1,41 @@
+
+installdir = $(bindir)
+
+install_files = \
+	pstamp_cleanup.pl \
+	pstamp_finish.pl \
+	pstamp_insert_request.pl \
+	pstamp_job_run.pl \
+	pstamp_listjobs.pl \
+	pstamp_new_request.sh \
+	pstampparse.pl \
+	pstamp_parser_run.pl \
+	pstamp_queue_cleanup.pl \
+	pstamp_queue_requests.pl \
+	pstamp_queue_update_cleanup.pl \
+	pstamp_request_file \
+	pstamp_results_file.pl \
+	pstamp_revert_request.pl \
+	pstamp_runcommand.sh \
+	pstamp_server_status \
+	pstamp_save_server_status.pl \
+	pstamp_webrequest.pl \
+        pstamp_get_image_job.pl \
+	psmkreq \
+	psgetcalibinfo \
+	psstatus \
+	pstampstopfaulted \
+	pstamp_checkdependent.pl \
+	request_finish.pl \
+	detectability_respond.pl \
+	detect_query_read \
+	detect_query_create \
+	detect_response_create \
+        dquery_job_run.pl \
+        dquery_finish.pl \
+        dqueryparse.pl \
+	fakedresponse.pl
+
+install_SCRIPTS = $(install_files)
+
+ACLOCAL_AMFLAGS = -I m4
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/detect_query_create
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/detect_query_create	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/detect_query_create	(revision 41103)
@@ -0,0 +1,374 @@
+#!/usr/bin/env perl
+
+# create a MOPS_DETECTABILITY_QUERY fits table from a text description file
+
+use warnings;
+use strict;
+
+use Astro::FITS::CFITSIO qw( :constants );
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+
+use constant EXTNAME => 'MOPS_DETECTABILITY_QUERY'; # Extension name for output table
+
+my ( $input,			# Name of input text file
+     $output,			# Name of output table
+     $query_id, 
+     $nostage,
+     $version,
+     );
+
+GetOptions(
+	   'input|i=s'    => \$input,
+	   'output|o=s'   => \$output,
+	   'query_id|q=s' => \$query_id,
+           'version|v=s'  => \$version,
+           'nostage'      => \$nostage,
+) or pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --input --output",
+           -exitval => 3) unless defined $input and defined $output;
+
+# Read what we've been given
+my $in;
+if ($input eq '-') {
+    $in = \*STDIN;
+} else {
+    open $in, "<$input" or die "cannot open $input for reading";
+}
+my %colData;
+my %headerData;
+my $numRows = read_data_for_table($in,'\s+', \%colData, \%headerData); 
+if (!$numRows) {
+    print STDERR "no data in $input\n";
+    exit 1;
+}
+
+# The header keywords
+my $header = [
+        { name =>  'QUERY_ID', 
+                    writetype => TSTRING, 
+                    comment => 'MOPS Query ID for this batch query',
+                    value => undef
+        },
+        { name =>  'EXTVER', 
+                    writetype => TSTRING, 
+                    comment => 'Extension version',
+                    value => undef
+        },
+        { name =>  'FPA_ID', 
+                    writetype => TSTRING, 
+                    comment => 'orginal FPA_ID used at ingest',
+                    value => undef
+        }, 
+        { name =>  'MJD-OBS', 
+                    writetype => TDOUBLE, 
+                    comment => 'starting time of the exposure, MJD',
+                    value => undef
+        },
+        { name =>  'FILTER', 
+                    writetype => TSTRING, 
+                    comment => 'effective filter use for the exposure',
+                    value => undef
+        },
+        { name =>  'OBSCODE', 
+                    writetype => TSTRING, 
+                    comment => 'site identifier (MPC observatory code)',
+                    value => undef
+        },
+        { name =>  'STAGE',
+                   writetype => TSTRING,
+                   comment => 'processing stage to examine',
+                   value => undef
+        }
+];
+
+# Validate header.
+
+if (defined($query_id)) {
+    $headerData{QUERY_ID} = $query_id;
+}
+unless (exists($headerData{QUERY_ID})) {
+    die "No QUERY_ID specified for header.";
+}
+
+unless (exists($headerData{EXTVER})) {
+    die "No EXTVER specified for header.";
+}
+if ($headerData{EXTVER} == 1) {
+    unless (exists($headerData{STAGE})) {
+	warn "No STAGE value specified in header. Assuming default value of 'diff'";
+	$headerData{STAGE} = 'diff';
+    }
+    foreach my $entry_ref (@{ $header }) {
+	my $name = $entry_ref->{name};
+	unless (exists($headerData{$name})) {
+	    die "Required header value $name not specified (try EXTVER=2?).";
+	}
+	$entry_ref->{value} = $headerData{$name};
+    }
+}    
+elsif ($headerData{EXTVER} == 2) {
+    unless (exists($headerData{STAGE})) {
+	warn "No STAGE value specified in header. Assuming default value of 'diff'";
+	$headerData{STAGE} = 'diff';
+    }
+    my $tmp_header;
+    foreach my $entry_ref (@{ $header }) {
+	my $name = $entry_ref->{name};
+	if (exists($headerData{$name})) {
+	    $entry_ref->{value} = $headerData{$name};
+	    push @{ $tmp_header }, $entry_ref;
+	}
+    }
+    $header = $tmp_header;
+}
+else {
+    die "Unknown EXTVER = $headerData{EXTVER}.";
+}
+
+# Specification of columns to write
+my $columns = [ 
+        # matching rownum from detectability original request
+        { name => 'ROWNUM',   type => '20A', writetype => TSTRING }, 
+        # coordinate at start of exposure, in degrees
+        { name => 'RA1_DEG',  type => 'D',   writetype => TDOUBLE },
+        # coordinate at start of exposure, in degrees
+        { name => 'DEC1_DEG', type => 'D',   writetype => TDOUBLE },
+        # coordinate at end of exposure, in degrees
+        { name => 'RA2_DEG',  type => 'D',   writetype => TDOUBLE },
+        # coordinate at end of exposure, in degrees
+        { name => 'DEC2_DEG', type => 'D',   writetype => TDOUBLE },
+        # apparent magnitude
+        { name => 'MAG',      type => 'D',   writetype => TDOUBLE },
+];
+my $columns_v2 = [
+    { name => 'FPA_ID', type => '20A', writetype => TSTRING },
+    { name => 'MJD-OBS', type => 'D', writetype => TDOUBLE },
+    { name => 'FILTER', type => '20A', writetype => TSTRING },
+    { name => 'OBSCODE', type => '20A', writetype => TSTRING },
+    { name => 'STAGE', type => '20A', writetype => TSTRING }
+    ];
+# Validate the data.
+if ($headerData{EXTVER} == 1) {
+    foreach my $entry_ref (@{ $columns }) {
+	my $name = $entry_ref->{name};
+	unless (exists($colData{$name})) {
+	    die "Required data column $name not found (try EXTVER=2?).";
+	}
+    }
+}
+if ($headerData{EXTVER} == 2) {
+    my $tmp_columns;
+    foreach my $entry_ref (@{ $columns }) {
+	my $name = $entry_ref->{name};
+	if (exists($colData{$name})) {
+	    push @{ $tmp_columns }, $entry_ref;
+	}
+    }
+    foreach my $entry_ref (@{ $columns_v2 }) {
+	my $name = $entry_ref->{name};
+	if (exists($colData{$name})) {
+	    push @{ $tmp_columns }, $entry_ref;
+	}
+    }
+    $columns = $tmp_columns;
+}	    
+
+# Construct the array of arrays
+my @colDataAoA = ();
+foreach my $col_def (@{ $columns }) {
+    my $name = $col_def->{name};
+    push @colDataAoA, $colData{$name};
+}
+
+# foreach (@$columns) {
+#     push @colData, [];
+# }
+
+
+
+my $status = make_fits_table($output, EXTNAME, $numRows, \@colDataAoA, $columns, $header);
+
+exit $status;
+
+# XXXXX: This should be in a module
+# two utility functions that may be used to create a FITS binary
+# table from hashes describing the header keywords and columns
+
+# read_table_description reads the data for a table from a simple text file
+# make_fits_table writes out the table to a named file
+
+use Astro::FITS::CFITSIO qw( :constants );
+Astro::FITS::CFITSIO::PerlyUnpacking(1);
+
+# A function to build a fits binary table from supplied data 
+# 
+sub make_fits_table {
+        my $output = shift;     # name of output file
+        my $extname = shift;    # extension name
+        my $numRows = shift;    # number of rows in the table
+        my $colData = shift;    # ref to array of arrays containing the data for each column
+        my $columns = shift;    # ref to array of column descriptions (each a hash)
+                                # with keys: name, type, and writetype
+        my $header = shift;     # ref to array of header keyword descriptions - each a hash
+                                # with keys: name, name, writetype, comment, and value
+        my $status = 0;
+
+        die "incorrect arguments" if !defined($columns);
+        # note $header can be nil
+
+        # build arrays for cfitsio
+        my @colNames;			# Names of columns
+        my @colTypes;			# Types of columns
+        my @colWriteType;               # type to use to write
+
+        foreach my $colSpec ( @$columns) {
+            push @colNames, $colSpec->{name};
+            push @colTypes, $colSpec->{type};
+            push @colWriteType, $colSpec->{writetype};
+        }
+
+        if (-e $output) {
+            unlink "$output" or die "failed to remove existing $output";
+        }
+
+        my $outFits = Astro::FITS::CFITSIO::create_file( $output, $status ); # Output file handle
+        check_fitsio( $status );
+
+        $outFits->create_img( 16, 0, undef, $status );
+        check_fitsio( $status );
+
+        # Create the table
+
+        $outFits->create_tbl( BINARY_TBL(), $numRows, scalar @colNames,
+                                \@colNames, \@colTypes, undef, $extname, $status );
+        check_fitsio( $status );
+
+        # if header keyword descriptions were provided add them
+        if ($header) {
+            foreach my $headerword ( @$header ) {
+                my $value = $headerword->{value};
+                unless (defined $value) {
+                    print "Can't find header keyword $headerword\n";
+                    next;
+                }
+                # zap quotation marks
+                $value =~ s/\'//g;
+                my $name    = $headerword->{name};
+                my $type    = $headerword->{writetype};
+                my $comment = $headerword->{comment};
+                $outFits->write_key( $type, $name, $value, $comment, $status );
+                check_fitsio( $status );
+            }
+        }
+
+
+        for (my $i = 0; $i < scalar @colNames; $i++) {
+            my $writeType = $colWriteType[$i];
+            $outFits->write_col( $writeType, $i + 1, 1, 1, $numRows, $colData->[$i], $status );
+            check_fitsio( $status );
+        }
+
+        $outFits->close_file( $status );
+
+        return 0;
+
+} # end of sub make_fits_table
+
+
+
+# read the table contents from a file
+#
+# input text file format:
+#   lines that begin with '#' are comment lines and are skipped.
+#   other lines are data. Each data line is split into fields with the
+#   provided separator
+#
+# if $header is not null header the first non-commented line is read to
+# fill the value for each header keyword. The number of fields must match
+# the number of keywords.
+#
+# Following the optional header data, each data line contains data for each
+# row in the table. The number of fields must match the number of column
+# arrays provided.
+
+sub read_data_for_table {
+    my $in      = shift;    # input file handle
+    my $sep     = shift;    # string containing field separator
+    my $colData = shift;    # reference to an array of arrays for the data
+    my $header  = shift;    # rerence to array of header keyword descriptions
+
+    my $line_num = 0;
+    my @keywords = ();
+    my $row_num;
+    while (<$in>) {
+	chomp;
+#	print STDERR "$line_num $#keywords $_\n";
+	if ($_ =~ /^\s*$/) {
+	    next;
+	}
+	if ($line_num == 0) {
+	    if ($_ !~ /EXTVER/) {
+		next;
+	    }
+	    # Parse header information keywords
+	    $_ =~ s/#//g;
+	    @keywords = split /\s+/;
+	    if ($keywords[0] eq '') {
+		shift(@keywords);
+	    }
+	}
+	elsif ($line_num == 1) {
+	    # Parse header information values
+	    my @values = split /\s+/;
+	    if ($#values != $#keywords) {
+		die "Number of header columns in input does not equal expected number of header words $#values $#keywords";
+	    }
+	    for (my $i = 0; $i <= $#values; $i++) {
+		$header->{$keywords[$i]} = $values[$i];
+	    }		
+	}
+	elsif ($line_num == 2) {
+	    if ($_ !~ /ROWNUM/) {
+		next;
+	    }
+	    # Parse table information keywords, dumping old keywords
+	    $_ =~ s/#//g;
+	    @keywords = split /\s+/;
+	    if ($keywords[0] eq '') {
+		shift(@keywords);
+	    }
+	}
+	else {
+	    # Parse table information values
+	    unless ($_ =~ /^#/) {
+		my @values = split /\s+/;
+		if ($#values != $#keywords) {
+		    die "Number of header columns in input does not equal expected number of header words $#values $#keywords";
+		}
+		for (my $i = 0; $i <= $#values; $i++) {
+		    push @{ $colData{$keywords[$i]} }, $values[$i];
+		    $row_num = $#{ $colData{$keywords[$i] } } + 1;
+		}		
+	    }
+	}
+	$line_num++;
+    }
+    # we return the number of rows read
+    return $row_num;
+}
+
+# From Astro::FITS::CFITSIO demo
+sub check_fitsio
+{
+    my $status = shift;		# Status of FITSIO calls
+
+    if ($status != 0) {
+	my $msg;		# Message to output
+	Astro::FITS::CFITSIO::fits_get_errstatus( $status , $msg );
+	die "CFITSIO error: $msg\n";
+    }
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/detect_query_read
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/detect_query_read	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/detect_query_read	(revision 41103)
@@ -0,0 +1,408 @@
+#!/usr/bin/env perl
+
+# Read and parse a fits table containing a MOPS_DETECTABILITY_QUERY Extension
+# and optionally print out the contents
+
+# by default prints out the keywords in the exension header followed by the rows of the table
+# with labels
+# -l omits the labels
+# -h omits the header
+# -r omits the rows
+# so -lhr will print nothing
+
+use warnings;
+use strict;
+
+use Astro::FITS::CFITSIO qw( :constants );
+Astro::FITS::CFITSIO::PerlyUnpacking(1);
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+use Math::Trig;
+use Data::Dumper;
+use IPC::Cmd 0.36 qw( can_run run );
+
+use constant EXTNAME => 'MOPS_DETECTABILITY_QUERY'; # Extension name for table
+
+my $no_print_label  = 0;    # omit the labels
+my $no_print_header = 0;    # omit the header keywords
+my $no_print_rows   = 0;    # omit the rows
+my $version = 0;
+
+my ( $input,			# Name of input text file
+     $output,			# Name of output table
+     $save_temps,		# Save temporary files?
+     $dbname,                   # needed to do camtool lookups.
+     );
+
+my $regtool = can_run('regtool') or (die "Can't find regtool");
+my $camtool = can_run('camtool') or (die "Can't find camtool");
+my $difftool = can_run('difftool') or (die "Can't find difftool");
+
+
+GetOptions(
+	   'input|i=s'    => \$input,
+	   'output|o=s'   => \$output,
+           'dbname=s'     => \$dbname,
+           'nolabel|l'    => \$no_print_label,
+           'noheader|h'   => \$no_print_header,
+           'norows|r'     => \$no_print_rows
+) or pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --input",
+           -exitval => 3) unless defined $input;
+
+if ($output && ($output ne "-")) {
+    die "cannot open $output for output\n" unless open OUTFILE, ">$output";
+    select OUTFILE;
+}
+
+my $status = 0;
+
+# Read the input file
+
+my $inFits = Astro::FITS::CFITSIO::open_file( $input, READONLY, $status ); # FITS file handle
+check_fitsio($status);
+
+$inFits->movnam_hdu(BINARY_TBL, EXTNAME, 0, $status) and check_fitsio($status);
+
+my $inHeader = $inFits->read_header(); # Header for input
+
+my $numRows;			# Number of rows in table
+$inFits->get_num_rows($numRows, $status) and check_fitsio($status);
+
+# The keywords found in the header
+
+my $parse_error = 0;
+# Parse the header to determine what we have to work with.
+# Clean up empty space and remove unneeded single quotes.
+foreach my $header_key (keys %{ $inHeader }) {
+    $inHeader->{$header_key} =~ s/\s+//g;
+    $inHeader->{$header_key} =~ s/\'//g;
+}
+my ($EXTVER,$headerEXTNAME,$QUERY_ID,
+    $FPA_ID,$MJD_OBS,$FILTER,
+    $OBSCODE,$STAGE) = 
+    ($inHeader->{EXTVER},$inHeader->{EXTNAME},$inHeader->{QUERY_ID},
+     $inHeader->{FPA_ID},$inHeader->{'MJD-OBS'},$inHeader->{FILTER},
+     $inHeader->{OBSCODE},$inHeader->{STAGE});
+
+unless(defined($EXTVER) && defined($headerEXTNAME) &&
+       (($EXTVER == 1)||($EXTVER == 2)) && 
+       ($headerEXTNAME eq EXTNAME)&&
+       (defined($QUERY_ID))) {
+    $parse_error = 1;
+}
+unless(($EXTVER == 2)||
+       ((defined($QUERY_ID))&&(defined($FPA_ID))&&(defined($MJD_OBS))&&
+	(defined($FILTER))&&(defined($OBSCODE)))) {
+    $parse_error = 2;
+}
+
+# Specification of columns
+my $column_defs = [ 
+        # matching rownum from detectability original request
+        { name => 'ROWNUM',   type => '20A', writetype => TSTRING, version => 1 }, 
+        # coordinate at start of exposure, in degrees
+        { name => 'RA1_DEG',  type => 'D',   writetype => TDOUBLE, version => 1 },
+        # coordinate at start of exposure, in degrees
+        { name => 'DEC1_DEG', type => 'D',   writetype => TDOUBLE, version => 1 },
+        # coordinate at end of exposure, in degrees
+        { name => 'RA2_DEG',  type => 'D',   writetype => TDOUBLE, version => 1 },
+        # coordinate at end of exposure, in degrees
+        { name => 'DEC2_DEG', type => 'D',   writetype => TDOUBLE, version => 1 },
+        # apparent magnitude
+        { name => 'MAG',      type => 'D',   writetype => TDOUBLE, version => 1 },
+    # v2 query_id: needs to be here.
+    { name => 'QUERY_ID', type => '20A', writetype => TSTRING, version => 2, default => $QUERY_ID },
+    # v2 fpa_id: original FPA_ID used at ingest
+    { name => 'FPA_ID', type => '20A', writetype => TSTRING, version => 2, default => $FPA_ID },
+    # v2 mjd obs: starting time of the exposure, MJD
+    { name => 'MJD-OBS', type => 'D', writetype => TDOUBLE, version => 2, default => $MJD_OBS },
+    # v2 filter: effective filter used for the exposure as a string, allowed values of g, r, i, z, y, B, V, w
+    { name => 'FILTER', type => '3A', writetype => TSTRING, version => 2, default => $FILTER },
+    # v2 obscode: site identifier (MPC observatory code)
+    { name => 'OBSCODE', type => '3A', writetype => TSTRING, version => 2, default => $OBSCODE },
+    # v2 stage: stage name to perform query on, allowed values of 'chip', 'warp', 'stack', and 'diff'
+    { name => 'STAGE', type => '20A', writetype => TSTRING, version => 2, default => $STAGE },
+];
+
+# Parse the list of columns
+my @colNames;			# Names of columns
+my @colTypes;			# Types of columns
+my @colWriteType;               # type to use to write
+my %colData;			# Data for each column referenced by name
+foreach my $colSpec ( @$column_defs) {
+    push @colNames, $colSpec->{name};
+    push @colTypes, $colSpec->{type};
+    push @colWriteType, $colSpec->{writetype};
+}
+
+foreach my $col (@$column_defs) {
+    my ($col_num, $col_type, $col_data);
+
+    if ($col->{version} > $EXTVER) {
+	@{ $colData{$col->{name}} } = map { $col->{default} } (0 .. $numRows - 1);
+	next;
+    }
+    else {
+	if (defined($col->{default})) {
+	    @{ $colData{$col->{name}} } = map { $col->{default} } (0 .. $numRows - 1);
+	    next;
+	}	 
+    }   
+
+    $inFits->get_colnum(0, $col->{name}, $col_num, $status);
+    if ($status == 0) {
+	$inFits->get_coltype($col_num, $col_type, undef, undef, $status) and check_fitsio($status);
+	$inFits->read_col($col_type, $col_num, 1, 1, $numRows, 0, $col_data, undef, $status)
+	    and check_fitsio($status);
+	$colData{$col->{name}} = $col_data;
+	if ($col->{name} eq 'QUERY_ID') {
+	    print @{ $col_data } . "\n";
+	}
+
+    }
+    elsif ($status == 219) {
+	@{ $colData{$col->{name}} } = map { "Not_Set" } (0 .. $numRows - 1);
+	$status = 0;
+    }
+}
+
+# Set things that aren't set.
+for (my $i = 0; $i < $numRows; $i++) {
+# Simple stuff first.
+    my %known_filters = ();
+    if ($colData{STAGE}[$i] eq "Not_Set") {
+	$colData{STAGE}[$i] = 'diff';
+    }
+    if ($colData{OBSCODE}[$i] eq "Not_Set") {
+	$colData{OBSCODE}[$i] = 566;
+    }
+# Define filter and MJD from FPA_ID
+    if (($colData{FILTER}[$i] eq "Not_Set")&&($colData{FPA_ID}[$i] ne "Not_Set")) {
+	if (exists($known_filters{$colData{FPA_ID}[$i]})) {
+	    $colData{FILTER}[$i] = $known_filters{$colData{FPA_ID}[$i]};
+	}
+	else {
+	    my $cmd = "$regtool -processedexp -dbname $dbname -exp_name $colData{FPA_ID}[$i]";
+	    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+		run(command => $cmd, verbose => 0);
+	    unless ($success) {
+		# This is a problem, because I'm not sure how we handle a failure to read something.
+		# We need to return a $PSTAMP_INVALID_REQUEST, I think, but if we can't read it, 
+		# we can't send that response back.
+		die("Unable to perform $cmd error code: $error_code");
+	    }
+	    foreach my $entry (split /\n/, (join "", @$full_buf)) {
+		$entry =~ s/^\s+//;
+		my @line = split /\s+/, $entry;
+		if (scalar(@line) != 3) {
+		    next;
+		}
+		my ($key,$type,$value) = @line;
+#		print "$entry => $key / $type / $value \n";
+		if ($key =~ /filter/i) {
+		    $value =~ s/.00000//;
+		    $colData{FILTER}[$i] = $value;
+		    $known_filters{$colData{FPA_ID}[$i]} = $value;
+		}
+	    }
+	}
+    }
+    # Calculate a MJD
+    if (($colData{'MJD-OBS'}[$i] eq "Not_Set")&&($colData{FPA_ID}[$i] ne "Not_Set")) {
+	# HACK!
+	my $mjd = $colData{FPA_ID}[$i];
+	$mjd =~ s/o(....)g.*/$1/;
+	$mjd += 50000;
+	$colData{'MJD-OBS'}[$i] = $mjd;
+    }
+    # Calculate an exp_id
+    if (($colData{FPA_ID}[$i] eq "Not_Set")&&(($colData{'FILTER'}[$i] ne "Not_Set")&&
+					      ($colData{'MJD-OBS'}[$i] ne "Not_Set"))) {
+        my $mjd_obs = int $colData{'MJD-OBS'}[$i];
+	my $dateobs_begin = mjd_to_dateobs($mjd_obs);
+	my $dateobs_end = mjd_to_dateobs($mjd_obs + 1);
+	my $ra = $colData{'RA1_DEG'}[$i];
+	my $dec = $colData{'DEC1_DEG'}[$i];
+	my $filter = $colData{'FILTER'}[$i] . ".00000";
+	my $cmd;
+	if ($colData{STAGE}[$i] eq 'SSdiff') {
+            # Assumption: All Stack Stack diffs are for a medium deep field
+            # go find which one the coordinates are in
+            # XXX: If we don't find one we'll just get the first SSdiff
+            # in the filter on the day in question
+            my $field = find_md_field($ra, $dec, 1.6);
+	    $cmd = "$difftool -listssrun -dbname $dbname -filter $filter ";
+	    $cmd .= " -mjd_obs_begin $mjd_obs -mjd_obs_end " . (1 + $mjd_obs);
+	    $cmd .= " -limit 1";
+            $cmd .= " -tess_id $field%" if $field;
+	}
+	elsif ($colData{STAGE}[$i] eq 'WSdiff') {
+	    $cmd = "$difftool -listrun -dbname $dbname -filter $filter ";
+	    $cmd .= " -dateobs_begin $dateobs_begin -dateobs_end $dateobs_end ";
+	    $cmd .= " -ra $ra -decl $dec -radius 1.5 -limit 1 -diff_mode 2";
+            $cmd .= " -diff_mode 2";
+	}
+	elsif ($colData{STAGE}[$i] eq 'diff') {
+	    $cmd = "$difftool -listrun -dbname $dbname -filter $filter ";
+	    $cmd .= " -dateobs_begin $dateobs_begin -dateobs_end $dateobs_end ";
+	    $cmd .= " -ra $ra -decl $dec -radius 1.5 -limit 1";
+	}
+	else {
+	    $cmd = "$camtool -processedexp -dbname $dbname -filter $filter ";
+	    $cmd .= " -dateobs_begin $dateobs_begin -dateobs_end $dateobs_end ";
+	    $cmd .= " -ra $ra -decl $dec -radius 1.5 -limit 1";
+	}
+	my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+	    run(command => $cmd, verbose => 1);
+	unless ($success) {
+	    # This is a problem, because I'm not sure how we handle a failure to read something.
+	    # We need to return a $PSTAMP_INVALID_REQUEST, I think, but if we can't read it, 
+	    # we can't send that response back.
+	    die("Unable to perform $cmd error code: $error_code");
+	}
+	foreach my $entry (split /\n/, (join "", @$full_buf)) {
+	    $entry =~ s/^\s+//;
+	    my @line = split /\s+/, $entry;
+	    if (scalar(@line) != 3) {
+		next;
+	    }
+	    my ($key,$type,$value) = @line;
+#		print "$entry => $key / $type / $value \n";
+	    if ($colData{STAGE}[$i] =~ /diff/) {
+		if ($key =~ /diff_id/i) {
+		    $colData{FPA_ID}[$i] = $value;
+		}
+	    }
+	    else {
+		if ($key =~ /exp_name/i) {
+		    $colData{FPA_ID}[$i] = $value;
+		}
+	    }
+	}
+    }
+    # Correct diff modes
+    if ($colData{STAGE}[$i] eq 'WSdiff') {
+	$colData{STAGE}[$i] = 'diff';
+    }
+    elsif ($colData{STAGE}[$i] eq 'SSdiff') {
+	$colData{STAGE}[$i] = 'diff';
+    }
+}
+
+# Verify uniqueness of important columns:
+my @unique_fields = ('QUERY_ID','OBSCODE','STAGE');
+foreach my $colName (@unique_fields) {
+    my %counter = ();
+    my $i = 0;
+    foreach my $row (@{ $colData{$colName} }) {
+	$counter{$row} = 1;
+    }
+    if (scalar(keys(%counter)) != 1) {
+	$parse_error = 3;
+    }
+}
+if ($parse_error) {
+    die "Unable to parse detectability query: $parse_error " . &error_message($parse_error) . "\n";;
+}
+
+# # Now produce the output
+
+
+if (!$no_print_label) {
+    print "# ";
+    foreach my $col (@$column_defs) {
+	printf "%-12s ", $col->{name};
+    }
+    print "\n";
+}
+
+for (my $i = 0; $i < $numRows; $i++) {
+    foreach my $col (@$column_defs) {
+	printf "%-12s ", $colData{$col->{name}}->[$i];
+        #foreach my $aref (@col_arrays) {
+	#printf "%-12s ", $aref->[$i];
+    }
+    print "\n";
+}
+
+exit 0;
+
+### Pau.
+
+# From Astro::FITS::CFITSIO demo
+sub check_fitsio
+{
+    my $status = shift;		# Status of FITSIO calls
+
+    if ($status != 0) {
+	my $msg;		# Message to output
+	Astro::FITS::CFITSIO::fits_get_errstatus( $status , $msg );
+	die "CFITSIO error: $status => $msg\n";
+    }
+}
+
+sub error_message {
+    my $error = shift;
+    if ($error == 1) {
+	return("Unknown EXTVER/EXTNAME");
+    }
+    if ($error == 2) {
+	return("Required header field not found");
+    }
+    if ($error == 3) {
+	return("Unique column not uniquely specified");
+    }
+    else {
+	return("Unknown fault.");
+    }
+}
+
+# Stolen from PStamp/Job.pm.  Thanks, Bill.
+sub mjd_to_dateobs {
+    my $mjd = shift;
+
+    my $ticks = ($mjd - 40587.0) * 86400;
+
+    my ($sec, $min, $hr, $day, $mon, $year) = gmtime($ticks);
+
+    return sprintf "'%4d-%02d-%02dT%02d:%02d:%02dZ'", $year+1900, $mon+1, $day, $hr, $min, $sec;
+}
+
+
+
+# Given an RA and  DEC return the MD field that contains it if any.
+# This is much faster than actuallly examining the tessellation
+# using dvoImagesAtCoords
+
+sub find_md_field {
+    my $ra_deg = shift;
+    my $dec_deg = shift;
+    my $radius_deg = shift;
+
+    my @MD     = qw(MD01  MD02    MD03     MD04      MD05    MD06      MD07    MD08    MD09     MD10);
+    my @MD_RA  = (35.875, 53.10, 130.592, 150.000, 161.917, 185.000, 213.454, 242.787, 333.98, 352.312);
+    my @MD_DEC = (-4.25,  -27.8,   44.317,   2.20,   58.083,  47.117,  53.243, 54.95,    0.283, -0.433);
+    my $PS_DEG_RAD = 0.017453292519943;
+
+    my $ra     = $ra_deg * $PS_DEG_RAD;
+    my $dec    = $dec_deg * $PS_DEG_RAD;
+    my $radius = $radius_deg * $PS_DEG_RAD;
+
+    for (my $i = 0; $i < scalar @MD; $i++) {
+        
+        my $dist = acos((cos($PS_DEG_RAD * $MD_DEC[$i]) * cos($dec) * cos($PS_DEG_RAD * $MD_RA[$i] - $ra)) +(sin($PS_DEG_RAD * $MD_DEC[$i]) * sin($dec)));
+
+        if ($dist < $radius) {
+            return $MD[$i];
+        }
+    }
+    print STDERR "failed to find MD field containing $ra_deg $dec_deg\n";
+
+    return undef;
+}
+
+
+__END__
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/detect_response_create
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/detect_response_create	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/detect_response_create	(revision 41103)
@@ -0,0 +1,176 @@
+#!/usr/bin/env perl
+
+# program to create a simulated MOPS_DETECTABILITY_RESPONSE file
+# based on text format input file
+
+use warnings;
+use strict;
+
+use Astro::FITS::CFITSIO qw( :constants );
+Astro::FITS::CFITSIO::PerlyUnpacking(1);
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+use Math::Trig;
+use Data::Dumper;
+
+use constant EXTNAME => 'MOPS_DETECTABILITY_RESPONSE'; # Extension name for output table
+use constant EXTVER =>  1;
+use constant OBSERVATORY_CODE => 566; # IAU Observatory Code
+
+my ( $input,			# Name of input Detectabilty Query table
+     $output,			# Name of output table
+     $save_temps,		# Save temporary files?
+     );
+
+GetOptions(
+	   'input|i=s'    => \$input,
+	   'output|o=s'   => \$output,
+	   'save-temps' => \$save_temps,
+) or pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --input --output",
+           -exitval => 3)
+    unless defined $input 
+    and defined $output;
+
+my $status = 0;
+
+# Specification of columns to write
+my $columns = [ 
+        # matching rownum from detectability original request
+        { name => 'ROWNUM',   type => '20A', writetype => TSTRING }, 
+
+        # number of pixels used in hypothetical PSF for the query detection
+        { name => 'DETECT_N', type => 'V',   writetype => TULONG },
+
+        # detectibility, indicating the fraction of PSF pixels detetable by IPP
+        { name => 'DETECT_F', type => 'D',   writetype => TDOUBLE },
+            ];
+
+# Header translation table
+my $headers = {
+    'QUERY_ID' => { name => 'QUERY_ID', type => TSTRING, 
+                        comment => 'MOPS Query ID for this batch query' },
+    'FPA_ID' => { name => 'FPA_ID',   type => TSTRING, 
+                        comment => 'original FPA_ID used at ingest' },
+	    };
+
+# Parse the list of columns
+my @colNames;			# Names of columns
+my @colTypes;			# Types of columns
+my %colData;			# Data for each column
+my @colWriteType;                 # type to use to write
+foreach my $colSpec ( @$columns) {
+    push @colNames, $colSpec->{name};
+    push @colTypes, $colSpec->{type};
+    push @colWriteType, $colSpec->{writetype};
+    $colData{$colSpec->{name}} = [];
+}
+
+my $in;
+if ($input eq "-") {
+    $in = *STDIN;
+} else {
+    die "cannot open input file $input" unless open $in, "<$input";
+}
+
+my $numRows;
+my $inHeader = { };
+
+read_input($in);
+
+
+# Write the output
+unlink "$output" if -e "$output";
+# Output file handle
+my $outFits = Astro::FITS::CFITSIO::create_file( $output, $status );
+check_fitsio( $status );
+
+$outFits->create_img( 16, 0, undef, $status );
+check_fitsio( $status );
+
+# Start the table
+$outFits->create_tbl( BINARY_TBL(), $numRows, scalar @colNames, \@colNames, \@colTypes, undef, EXTNAME,
+		      $status );
+check_fitsio( $status );
+
+# TODO: get the extension version number from somewhere common
+$outFits->write_key( TINT, 'EXTVER', EXTVER, 'Version of this Extension', $status );
+check_fitsio( $status );
+
+# Write the Extension keywords
+foreach my $keyword ( keys %$headers ) {
+    my $value = $inHeader->{$keyword}->{value}; # Header keyword value
+    unless (defined $value) {
+	print "Can't find header keyword $keyword\n";
+	next;
+    }
+    $value =~ s/\'//g;
+    my $name    = $headers->{$keyword}->{name}; # New name
+    my $type    = $headers->{$keyword}->{type}; # Type
+    my $comment = $headers->{$keyword}->{comment}; # Comment
+    $outFits->write_key( $type, $name, $value, $comment, $status );
+    check_fitsio( $status );
+}
+
+
+for (my $i = 0; $i < scalar @colNames; $i++) {
+    my $colName = $colNames[$i];# Column name
+    my $writeType = $colWriteType[$i];
+    $outFits->write_col( $writeType, $i + 1, 1, 1, $numRows, $colData{$colName}, $status );
+    check_fitsio( $status );
+}
+
+$outFits->close_file( $status );
+
+### Pau.
+
+
+# From Astro::FITS::CFITSIO demo
+sub check_fitsio
+{
+    my $status = shift;		# Status of FITSIO calls
+
+    if ($status != 0) {
+	my $msg;		# Message to output
+	Astro::FITS::CFITSIO::fits_get_errstatus( $status , $msg );
+	die "CFITSIO error: $msg\n";
+    }
+}
+
+sub read_input
+{
+    my $inh = shift;
+    my $line;
+    # read data for header
+    while ($line = <$inh>) {
+        next if ($line =~ /^#/);    # skip comment lines
+        chomp $line;
+        ($inHeader->{QUERY_ID}->{value}, $inHeader->{FPA_ID}->{value}, $inHeader->{MJD_OBS}->{value}, 
+         $inHeader->{FILTER}->{value}, $inHeader->{OBSCODE}->{value}) 
+                = split " ", $line;
+        last;
+    }
+    die "failed to read valid header words from $input" 
+        unless defined($inHeader->{QUERY_ID}->{value}) &&
+           ($inHeader->{FPA_ID}->{value}) &&
+           ($inHeader->{MJD_OBS}->{value}) && ($inHeader->{FILTER}->{value}) &&
+           ($inHeader->{OBSCODE}->{value}) ;
+
+
+    $numRows = 0;
+    while ($line = <$inh> ) {
+        next if ($line =~ /^#/);    # skip comment lines
+        chomp $line;
+
+        my ($rownum, $npix, $flux) = split " ", $line;
+
+        push @{$colData{'ROWNUM'}},   $rownum;
+        push @{$colData{'DETECT_N'}}, $npix;
+        push @{$colData{'DETECT_F'}}, $flux;
+        $numRows++;
+    }
+}
+
+__END__
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/detectability_respond.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/detectability_respond.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/detectability_respond.pl	(revision 41103)
@@ -0,0 +1,891 @@
+#! /usr/bin/env perl
+#
+#
+# Create a response to a MOPS_DETECTABILITY_QUERY
+#
+#
+
+use strict;
+use warnings;
+
+use Sys::Hostname;
+use Carp;
+use File::Basename;
+use File::Copy;
+use IPC::Cmd 0.36 qw( can_run run );
+use Getopt::Long qw( GetOptions );
+use Pod::Usage qw( pod2usage );
+use File::Temp qw( tempfile tempdir);
+
+use PS::IPP::PStamp::RequestFile qw( :standard );
+use PS::IPP::PStamp::Job qw( :standard );
+use PS::IPP::Config qw( :standard );
+use PS::IPP::Metadata::List qw( parse_md_list );
+
+use Astro::FITS::CFITSIO qw( :constants );
+Astro::FITS::CFITSIO::PerlyUnpacking(1);
+
+#
+# Set up
+#
+my $host = hostname();
+
+my $EXTVER = 1.0;
+my $EXTNAME = 'MOPS_DETECTABILITY_RESPONSE';
+my ($req_id,$job_id,$req_name,$product,$need_magic,$missing_tools,$project);
+my ($request_file,$output,$workdir,$dbname,$dbserver,$verbose,$save_temps,$ignore_wisdom);
+GetOptions(
+    'input=s'         =>      \$request_file,
+    'output=s'        =>      \$output,
+    'workdir=s'       =>      \$workdir,
+    'job_id=s'        =>      \$job_id,
+    'dbname=s'        =>      \$dbname,
+    'dbserver=s'      =>      \$dbserver,
+    'verbose'         =>      \$verbose,
+    'save-temps'      =>      \$save_temps,
+    'ignore-wisdom'   =>      \$ignore_wisdom,
+    ) or pod2usage(2);
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --input --output --dbname",
+	   -exitval => 3,
+    ) unless
+    defined $request_file and defined $output and defined $workdir and defined $dbname;
+
+my $detect_query_read = can_run('detect_query_read') or (warn "Can't find detect_query_read" and $missing_tools = 1);
+my $psphotForced      = can_run('psphotForced') or (warn "Can't find psphotForced" and $missing_tools = 1);
+my $dquery_finish     = can_run('dquery_finish.pl') or (warn "Can't find dquery_finish.pl" and $missing_tools = 1);
+my $ppCoord           = can_run('ppCoord') or (warn "Can't find ppCoord" and $missing_tools = 1);
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit($PS_EXIT_CONFIG_ERROR);
+}
+
+unless (defined $verbose) {
+    $verbose = 0;
+}
+
+my $ipprc = PS::IPP::Config->new();
+my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files
+
+
+if (!$dbserver) {
+    $dbserver = metadataLookupStr($ipprc->{_siteConfig}, 'PS_DBSERVER');
+}
+# This is hardcoded in for the moment.
+$project = resolve_project($ipprc,"gpc1",$dbname,$dbserver);
+my $imagedb = $project->{dbname};
+if (!$imagedb) {
+    carp("failed to find imagedb for project: $project");
+}
+
+my %query = ();
+my %image_list_hash = ();
+my $wisdom_file = "${workdir}/wisdom.dat";
+if ((-e $wisdom_file)&&!($ignore_wisdom)) {
+    print "Reading wisdom file $wisdom_file instead of parsing...\n";
+    open(WISDOM,"$wisdom_file") or my_die("failed to open wisdom file $wisdom_file");
+    while(<WISDOM>) {
+	chomp;
+	my ($fpa_id,@key_values) = split /\s+/;
+	my $index = $#{ $query{$fpa_id}{ROWNUM} } + 1;
+	while ($#key_values > -1) {
+	    my $key = shift(@key_values);
+	    my $val = shift(@key_values);
+	    # if we have wisdom, then we should have updated already.  If not, we'll bomb out later in the code.
+	    if ($key eq 'FAULT') {
+		$val = 0;
+	    }
+	    $query{$fpa_id}{$key}[$index] = $val;
+	}
+    }
+    close(WISDOM);
+} # End reading wisdom.
+else {
+    #
+    # Parse input request file using detect_query_read (as it's already written).
+    #
+    my $dqr_command = "$detect_query_read --dbname $imagedb --input $request_file";
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+	run(command => $dqr_command, verbose => $verbose);
+    unless ($success) {
+	# This is a problem, because I'm not sure how we handle a failure to read something.
+	# We need to return a $PSTAMP_INVALID_REQUEST, I think, but if we can't read it, 
+	# we can't send that response back.
+	die("Unable to perform $dqr_command error code: $error_code");
+    }
+    my $Nrows = 0;
+    {
+	my @column_names = ();
+        # split output into lines skip until the line which lists the column names is found
+        # Parse subsequent lines
+	foreach my $entry (split /\n/, (join "", @$stdout_buf)) {
+	    if ($entry =~ /^#/) {
+		@column_names = split /\s+/, $entry;
+		shift(@column_names);  # Dump the hash sign.
+	    }
+	    elsif (scalar @column_names) {
+		# ROWNUM RA1_DEG DEC1_DEG RA2_DEG DEC2_DEG MAG QUERY_ID FPA_ID MJD-OBS FILTER OBSCODE STAGE
+		my %row_data;
+		@row_data{@column_names} = (split /\s+/, $entry);
+		for (my $i = 0; $i <= $#column_names; $i++) {
+                    push @{ $query{$row_data{"FPA_ID"}}{$column_names[$i]} }, $row_data{$column_names[$i]};
+                    $Nrows = scalar(keys(%query));
+                    # print "$row_data{'FPA_ID'} $Nrows $i $column_names[$i] $row_data{$column_names[$i]}\n";
+                }
+	    }
+	}
+    }
+    #
+    # Identify target images.  This should properly collate targets on a single imfile.
+    #
+    foreach my $fpa_id (keys %query) {
+	my %temp_hash;
+	my $query_style = 'byexp';
+	my $stage;
+	my $filter;
+	my $mjd;
+	# Confirm that we only have one stage/filter/mjd
+	if ($fpa_id ne 'Not_Set') {   # We only need to check things that aren't the known odd case.
+	    for (my $i = 0; $i <= $#{ $query{$fpa_id}{STAGE} }; $i++) {
+		$temp_hash{STAGE}{$query{$fpa_id}{STAGE}[$i]} = 1;
+		$temp_hash{FILTER}{$query{$fpa_id}{FILTER}[$i]} = 1;
+		$temp_hash{'MJD-OBS'}{$query{$fpa_id}{'MJD-OBS'}[$i]} = 1;
+	    }
+	    if (scalar(keys(%{ $temp_hash{STAGE} })) == 1) {
+		$stage = (keys(%{ $temp_hash{STAGE} }))[0];
+	    }
+	    else {
+		exit_with_failure(21,"Too many STAGEs specified");
+	    }
+	    if (scalar(keys(%{ $temp_hash{FILTER} })) == 1) {
+		$filter = (keys(%{ $temp_hash{FILTER} }))[0];
+	    }
+	    else {
+		exit_with_failure(21,"Too many FILTERs specified");
+	    }
+	    if (scalar(keys(%{ $temp_hash{'MJD-OBS'} })) == 1) {
+		$mjd = (keys(%{ $temp_hash{'MJD-OBS'} }))[0];
+	    }
+	    else {
+		exit_with_failure(21,"Too many MJD-OBS specified");
+	    }
+	}
+
+	# Set up a rowList with default values
+	my @rowList;
+	for (my $i = 0; $i <= $#{ $query{$fpa_id}{STAGE} }; $i++) {
+	    $rowList[$i]->{CENTER_X} = $query{$fpa_id}{RA1_DEG}[$i];
+	    $rowList[$i]->{CENTER_Y} = $query{$fpa_id}{DEC1_DEG}[$i];
+	    $rowList[$i]->{ID} = $query{$fpa_id}{ROWNUM}[$i];
+	    $rowList[$i]->{COORD_MASK} = 0;
+	    # Set default values
+	    $query{$fpa_id}{BAD_COMPONENT}[$i] = 1;
+	    $query{$fpa_id}{IMAGE}[$i] = 'no_image';
+	    $query{$fpa_id}{MASK}[$i] = 'no_mask';
+	    $query{$fpa_id}{WEIGHT}[$i] = 'no_weight';
+	    $query{$fpa_id}{PSF}[$i] = 'no_psf';
+
+	    $query{$fpa_id}{STAGE_ID}[$i] = 'no_id';
+	    $query{$fpa_id}{IMAGE_DB}[$i] = 'no_imdb';
+	    $query{$fpa_id}{NEED_MAGIC}[$i] = 'no_magic';
+	    $query{$fpa_id}{MAGICKED}[$i] = 'no_magic';
+	    $query{$fpa_id}{CATALOG}[$i] = 'no_catalog';
+	    $query{$fpa_id}{COMPONENT_ID}[$i] = 'no_component';
+	    $query{$fpa_id}{CLASS_ID}[$i] = 'no_class';
+
+	    $query{$fpa_id}{STATE}[$i] = 'no_state';
+	    $query{$fpa_id}{DATA_STATE}[$i] = 'no_dstate';
+	    $query{$fpa_id}{FAULT}[$i] = 'no_fault';
+	    $query{$fpa_id}{BURNTOOL_STATE}[$i] = 'no_btstate';
+	}
+
+	# Determine the query style for this fpa_id
+	if ($fpa_id =~ /o.*g.*o/) {
+	    $query_style = 'byexp';
+	}
+	elsif ($fpa_id =~ /\d+/) {
+	    $query_style = 'byid';
+	}
+	elsif ($fpa_id eq 'Not_Set') {
+            # no matching file was found skip
+	    next;
+	}
+	else {
+	    exit_with_failure(21,"Parse error in request file");
+	}
+	# Set common request components
+	my $option_mask |= 1;
+	$option_mask |= $PSTAMP_SELECT_IMAGE;
+	$option_mask |= $PSTAMP_SELECT_MASK;
+	$option_mask |= $PSTAMP_SELECT_VARIANCE;
+	$option_mask |= $PSTAMP_SELECT_PSF;
+	my $need_magic = 1;
+	if ($stage eq 'stack') {
+	    $need_magic = 0;
+	}
+	my $mjd_min = int $mjd;
+	my $mjd_max = $mjd + 1;
+	
+        my $req_filter;
+        if ($filter ne 'Not_Set') {
+            $req_filter = $filter . '%';
+        }
+	
+	# Call the PStamp code to find the images that contain the target on the given MJD in the specified filter.
+	my $pstamp_images_ref = locate_images($ipprc,$imagedb,
+					  \@rowList,
+					  $query_style,$stage,
+					  $fpa_id,undef,undef,
+					  $option_mask,$need_magic,
+					  $mjd_min,$mjd_max,$req_filter,undef,$verbose);  
+	
+	foreach my $this_image_ref (@{ $pstamp_images_ref }) {
+
+            if (0) {
+                foreach my $key (sort (keys %{ $this_image_ref } )) {
+                    my $value = ${ $this_image_ref }{$key};
+                    if ($key eq 'row_index') {
+                        $value = join ' ', @{ $this_image_ref->{$key} };
+                    }
+                    print "$this_image_ref $key $value\n";
+                }
+            }
+
+
+            foreach my $valid_index (@{ $this_image_ref->{row_index} }) {
+                $query{$fpa_id}{IMAGE}[$valid_index] = $this_image_ref->{image};
+                $query{$fpa_id}{MASK}[$valid_index] = $this_image_ref->{mask};
+                $query{$fpa_id}{WEIGHT}[$valid_index] = $this_image_ref->{weight};
+                $query{$fpa_id}{PSF}[$valid_index] = $this_image_ref->{psf};
+                $query{$fpa_id}{STAGE_ID}[$valid_index] = $this_image_ref->{stage_id};
+                $query{$fpa_id}{IMAGE_DB}[$valid_index] = $this_image_ref->{imagedb};
+                $query{$fpa_id}{NEED_MAGIC}[$valid_index] = $need_magic;
+                $query{$fpa_id}{BAD_COMPONENT}[$valid_index] = 0;
+                
+                if (exists($this_image_ref->{astrom})) {
+                    $query{$fpa_id}{CATALOG}[$valid_index] = $this_image_ref->{astrom};
+                }
+                else {
+                    $query{$fpa_id}{CATALOG}[$valid_index] = $this_image_ref->{cmf};
+                }
+                if (exists($this_image_ref->{class_id})) {
+                    $query{$fpa_id}{COMPONENT_ID}[$valid_index] = $this_image_ref->{class_id};
+                    $query{$fpa_id}{CLASS_ID}[$valid_index] = $this_image_ref->{class_id};
+                    
+                }
+                else {
+                    $query{$fpa_id}{COMPONENT_ID}[$valid_index] = $this_image_ref->{skycell_id};
+                    $query{$fpa_id}{CLASS_ID}[$valid_index] = 'fpa';
+                }
+                
+                $query{$fpa_id}{STATE}[$valid_index] = $this_image_ref->{state};
+                if (exists($this_image_ref->{data_state})) {
+                    $query{$fpa_id}{DATA_STATE}[$valid_index] = $this_image_ref->{data_state};
+                }
+                else {
+                    $query{$fpa_id}{DATA_STATE}[$valid_index] = $this_image_ref->{state};
+                }
+                $query{$fpa_id}{FAULT}[$valid_index] = 0;
+		    $query{$fpa_id}{MAGICKED}[$valid_index] = $this_image_ref->{magicked};
+                if ($stage eq 'chip') {
+                    $query{$fpa_id}{BURNTOOL_STATE}[$valid_index] = $this_image_ref->{burntool_state};
+                }
+                
+                # Determine if the data exists.
+                if (($query{$fpa_id}{STATE}[$valid_index] eq 'goto_purged') or 
+                    ($query{$fpa_id}{DATA_STATE}[$valid_index] eq 'purged') or
+                    ($query{$fpa_id}{STATE}[$valid_index] eq 'drop') or 
+                    ($query{$fpa_id}{STATE}[$valid_index] eq 'error_cleaned') or 
+                    ($query{$fpa_id}{STATE}[$valid_index] eq 'goto_scrubbed') or 
+                    ($query{$fpa_id}{DATA_STATE}[$valid_index] eq 'scrubbed')) {
+                    
+                    # image is gone and it's not coming back
+                    $query{$fpa_id}{FAULT}[$valid_index] = $PSTAMP_GONE;
+                }
+                elsif ($need_magic and ($query{$fpa_id}{MAGICKED}[$valid_index] eq 0)) {
+                    $query{$fpa_id}{FAULT}[$valid_index] = $PSTAMP_NOT_DESTREAKED;
+                }
+                elsif (($query{$fpa_id}{DATA_STATE}[$valid_index] ne 'full')) {		      
+                    $query{$fpa_id}{FAULT}[$valid_index] = $PSTAMP_NOT_AVAILABLE;
+                    
+                    # updating stacks isn't implemented
+                    if (($stage eq 'stack')) {
+                        $query{$fpa_id}{FAULT}[$valid_index] = $PSTAMP_NOT_IMPLEMENTED;
+                    }
+                    # updating old burntool data isn't implemented
+                    elsif ($stage eq 'chip') {
+                        if ($query{$fpa_id}{BURNTOOL_STATE}[$valid_index] and 
+                            (abs($query{$fpa_id}{BURNTOOL_STATE}[$valid_index]) < 14)) {
+                            $query{$fpa_id}{FAULT}[$valid_index] = $PSTAMP_NOT_IMPLEMENTED;
+			    }
+                    }
+                } # End determining error faults.
+            }
+        }
+    }
+} # End calculating wisdom
+my %update_request;
+my %processing_request;
+
+if ($job_id) {
+    $wisdom_file = "$wisdom_file.$job_id";
+}
+open(WISDOM,">$wisdom_file") or my_die("failed to open wisdom file $wisdom_file");
+foreach my $fpa_id (keys %query) {
+    for (my $i = 0; $i <= $#{ $query{$fpa_id}{ROWNUM} }; $i++) {
+        print WISDOM "$fpa_id\t";
+        foreach my $key (keys %{ $query{$fpa_id} }) {
+            print WISDOM "$key $query{$fpa_id}{$key}[$i]\t";
+        }
+        print WISDOM "\n";
+        my $data_state = $query{$fpa_id}{DATA_STATE}[$i];
+        if ($query{$fpa_id}{BAD_COMPONENT}[$i] == 0) {
+            if ($data_state ne 'full') {
+                
+                @{ $update_request{$query{$fpa_id}{IMAGE}[$i]}{$query{$fpa_id}{FAULT}[$i]} } = 
+                    ($query{$fpa_id}{STATE}[$i],$query{$fpa_id}{STAGE}[$i],$query{$fpa_id}{STAGE_ID}[$i],
+                     $query{$fpa_id}{COMPONENT_ID}[$i],$query{$fpa_id}{NEED_MAGIC}[$i],$query{$fpa_id}{IMAGE_DB}[$i]);
+            }
+            push @{ $processing_request{$fpa_id}{$query{$fpa_id}{IMAGE}[$i]} }, $i;
+        }
+    }
+}
+close(WISDOM);
+
+if (! defined $job_id) {
+    # If there is anything that needs to be updated, create the update request list, and then exit the program.
+    my $exit_code = 0;
+    my $update_request_file = "${workdir}/update_request.dat";
+    open(UPDATE_REQUEST,">$update_request_file") or my_die("failed to open update request_file $update_request_file");
+    foreach my $images (keys %update_request) {
+        foreach my $fault (keys %{ $update_request{$images} }) {
+            if ($fault == 25) {
+                $exit_code = 25;
+            }
+            elsif ($fault != 0) {
+                $exit_code = 21;
+            }
+            my $update_request = join ' ', @{ $update_request{$images}{$fault} };
+            print UPDATE_REQUEST "$update_request\n";
+        }
+    }
+    close(UPDATE_REQUEST);
+    if ($exit_code != 0) {
+        exit($exit_code);
+    }
+}
+
+# This duplicates stuff returned by PSTAMP, but my thought is to convert that ---^ into a conditional, in which I read
+# from the wisdom.dat file.  This means only one pass on all that potentially slow stuff.  If that's the case, then 
+# I can recalculate the processing request.
+
+foreach my $fpa_id (keys %processing_request) {
+    foreach my $image (keys %{ $processing_request{$fpa_id} }) {
+	print "$fpa_id $image\t";
+	foreach my $i (@{ $processing_request{$fpa_id}{$image} }) {
+	    print "$i ";
+	}
+	print "\n";
+    }
+}
+
+# run ppCoord and psphotForced to calculate the required data.
+foreach my $fpa_id (keys %processing_request) {
+    foreach my $image (keys %{ $processing_request{$fpa_id} }) {
+	# Get this image specific data from the first entry. That entry is now the king of this set.
+	my $index = $processing_request{$fpa_id}{$image}[0];
+	my $fault = $query{$fpa_id}{FAULT}[$index];
+	my $catalog = $query{$fpa_id}{CATALOG}[$index];
+	my $psf   = $query{$fpa_id}{PSF}[$index];
+	my $mask  = $query{$fpa_id}{MASK}[$index];
+	my $weight= $query{$fpa_id}{WEIGHT}[$index];
+	my $stage = $query{$fpa_id}{STAGE}[$index];
+	my $stage_id = $query{$fpa_id}{STAGE_ID}[$index];
+	my $component = $query{$fpa_id}{COMPONENT_ID}[$index];
+        # print "Input is from $stage $stage_id $component\n";
+
+	# if there's a fault or quality problem, then we can't process this image.
+	# if (($fault != 0)||($query{$fpa_id}{BAD_COMPONENT}[$index] == 1)) {
+	if ($fault = check_component($stage, $stage_id, $component, $imagedb)) {
+            foreach my $i (@{ $processing_request{$fpa_id}{$image} }) {
+                $query{$fpa_id}{PROC_ERROR}[$i] = $fault;
+                
+                $query{$fpa_id}{NPIX}[$i] = 0;
+                $query{$fpa_id}{QFACTOR}[$i] = 0.0;
+                $query{$fpa_id}{FLUX}[$i] = 0.0;
+                $query{$fpa_id}{FLUX_SIG}[$i] = 0.0;
+            }
+
+	    next;
+	}
+
+	# Create coordinate file to convert to positions.
+	my ($coordfile,$coordname) = tempfile("${workdir}/detect.coords.$index.XXXX", 
+					      UNLINK => !$save_temps);
+	my ($targetfile,$targetname) = tempfile("${workdir}/detect.targets.$index.XXXX", 
+						UNLINK => !$save_temps);
+	foreach my $i (@{ $processing_request{$fpa_id}{$image} }) {
+	    print $coordfile "$query{$fpa_id}{RA1_DEG}[$i] $query{$fpa_id}{DEC1_DEG}[$i]\n";
+	}
+	
+	# Convert the sky coordinates to image coordinates with ppCoord.
+	my $command = "ppCoord -astrom $catalog -radec $coordname";
+	my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+	    run(command => $command, verbose => $verbose);
+	unless ($success) {
+	    my_die("Unable to perform $command. Error_code: $error_code",
+		   $query{$fpa_id}{QUERY_ID}[$index],$fpa_id,$query{$fpa_id}{'MJD-OBS'}[$index],
+		   $query{$fpa_id}{FILTER}[$index],$query{$fpa_id}{OBSCODE}[$index],$query{$fpa_id}{STAGE}[$index],
+		   $error_code);
+	}
+	my @response = split /\n/, (join "", @$stdout_buf);
+	my $i = 0;
+	foreach my $line (@response) {
+	    my ($r_ra,$r_dec,$trash,$r_x,$r_y,$r_chip) = split /\s+/, $line;
+	    print $targetfile "$r_x $r_y\n";
+	    if ((abs($r_ra - $query{$fpa_id}{RA1_DEG}[$processing_request{$fpa_id}{$image}[$i]]) < 1e-8)&&
+		(abs($r_dec - $query{$fpa_id}{DEC1_DEG}[$processing_request{$fpa_id}{$image}[$i]]) < 1e-8)) {
+		$query{$fpa_id}{X_PXL}[$processing_request{$fpa_id}{$image}[$i]] = $r_x;
+		$query{$fpa_id}{Y_PXL}[$processing_request{$fpa_id}{$image}[$i]] = $r_y;
+		$query{$fpa_id}{EXTENSION_BASE}[$processing_request{$fpa_id}{$image}[$i]] = $r_chip;
+	    }
+	    else {
+		$error_code = $PS_EXIT_PROG_ERROR;
+		my_die("Unable to match input RA/DEC with output RA/DEC: ($query{$fpa_id}{RA1_DEG}[$i],$query{$fpa_id}{DEC1_DEG}[$i]) -> ($r_ra,$r_dec) $error_code",
+		   $query{$fpa_id}{QUERY_ID}[$index],$fpa_id,$query{$fpa_id}{'MJD-OBS'}[$index],
+		   $query{$fpa_id}{FILTER}[$index],$query{$fpa_id}{OBSCODE}[$index],$query{$fpa_id}{STAGE}[$index],
+		   $error_code);
+	    }
+	    $i++;
+	}
+
+	# Run psphotForced on the target list.
+	my $tmpdir  = tempdir("detect.$index.XXXX", DIR => "${workdir}/", CLEANUP => !$save_temps);
+	my $outroot = "$tmpdir/detectability.${stage}.${fpa_id}.${index}";
+	$query{$fpa_id}{PROC_ERROR}[$index] = 0;
+	my $psphot_cmd = "$psphotForced -psf $psf ";
+	$psphot_cmd .= "-file $image ";
+	$psphot_cmd .= "-mask $mask ";
+	$psphot_cmd .= "-variance $weight ";
+	$psphot_cmd .= "-srctext $targetname $outroot ";
+	$psphot_cmd .= "-D OUTPUT.FORMAT PS1_DV1 ";
+	
+	( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+	    run(command => $psphot_cmd, verbose => $verbose);
+	unless ($success) {
+	    $query{$fpa_id}{PROC_ERROR}[$index] = $PSTAMP_SYSTEM_ERROR;
+	}
+	
+	# Why not parse out results here?
+# Convert psphot output to response
+	if ($query{$fpa_id}{PROC_ERROR}[$index] == 0) {
+	    my $class_id = $query{$fpa_id}{CLASS_ID}[$index];
+	    my $cmf = "${outroot}.${class_id}.cmf";
+	    my ($tmp_Npix,$tmp_Qfact,$tmp_flux,$tmp_flux_error) = read_cmf_file($cmf,$query{$fpa_id}{EXTENSION_BASE}[$index]);
+	    foreach ($i = 0; $i <= $#{ $processing_request{$fpa_id}{$image} }; $i++) {
+		my $result_index = $processing_request{$fpa_id}{$image}[$i];
+		$query{$fpa_id}{PROC_ERROR}[$result_index] = $query{$fpa_id}{PROC_ERROR}[$index];
+
+		$query{$fpa_id}{NPIX}[$result_index] = ${ $tmp_Npix }[$i];
+		$query{$fpa_id}{QFACTOR}[$result_index] = ${ $tmp_Qfact }[$i];
+		$query{$fpa_id}{FLUX}[$result_index] = ${ $tmp_flux }[$i];
+		$query{$fpa_id}{FLUX_SIG}[$result_index] = ${ $tmp_flux_error }[$i];
+		
+#		print "$fpa_id $image $#{ $processing_request{$fpa_id}{$image} } $result_index $i ${ $tmp_Npix }[$i]\n";
+	    }
+	}
+	else {
+	    foreach ($i = 0; $i <= $#{ $processing_request{$fpa_id}{$image} }; $i++) {
+		my $result_index = $processing_request{$fpa_id}{$image}[$i];
+		$query{$fpa_id}{PROC_ERROR}[$result_index] = $query{$fpa_id}{PROC_ERROR}[$index];
+
+		$query{$fpa_id}{NPIX}[$result_index] = 0;
+		$query{$fpa_id}{QFACTOR}[$result_index] = 0.0;
+		$query{$fpa_id}{FLUX}[$result_index] = 0.0;
+		$query{$fpa_id}{FLUX_SIG}[$result_index] = 0.0;
+	    }
+	}
+
+	    	
+    }
+}
+
+write_response_file($output,\%query);
+
+#
+# Utilities
+#
+
+# Taken largely from detect_query_read
+sub read_cmf_file {
+    my $cmf_file = shift;
+    my $class_id = shift;
+    my $extname = $class_id . ".psf";
+
+    my @tmp_Npix = ();
+    my @tmp_Qfact = ();
+    my @tmp_flux = ();
+    my @tmp_flux_err = ();
+
+    my ($detect_F_col,$detect_N_col,$detect_flux_col,$detect_mag_col) = (-1, -1, -1, -1);
+
+    
+    my $status = 0;
+    my $inFits = Astro::FITS::CFITSIO::open_file( $cmf_file, READONLY, $status ); # Open CMF
+    check_fitsio($status);
+    $inFits->movnam_hdu(BINARY_TBL, $extname, 0, $status) and check_fitsio($status);
+
+    my $inHeader = $inFits->read_header();
+
+    my $CMFversion = $inHeader->{EXTTYPE};
+    $CMFversion =~ s/\'//g;
+    $CMFversion =~ s/\s+//g;
+
+    # This is the only data we actually care about
+    my $column_defs;
+    if ($CMFversion eq 'PS1_V2') {
+	$column_defs = [
+	    { name => 'PSF_QF',   type => '1E', writetype => TDOUBLE },
+	    { name => 'PSF_NPIX', type => '1J', writetype => TLONG },
+	    { name => 'PSF_INST_MAG', type => '1E', writetype => TDOUBLE },
+	    { name => 'PSF_INST_MAG_SIG', type => '1E', writetype => TDOUBLE },
+	    ];
+    }
+    elsif ($CMFversion eq 'PS1_DV1') {
+	$column_defs = [
+	    { name => 'PSF_QF',   type => '1E', writetype => TDOUBLE },
+	    { name => 'PSF_NPIX', type => '1J', writetype => TLONG },
+	    { name => 'PSF_INST_FLUX', type => '1E', writetype => TDOUBLE },
+	    { name => 'PSF_INST_FLUX_SIG', type => '1E', writetype => TDOUBLE },
+	    ];
+    }
+
+    my @colNames; 
+    my @colTypes;
+    my @colWriteType;
+    my %colData;
+    foreach my $colSpec (@$column_defs) {
+	push @colNames, $colSpec->{name};
+	push @colTypes, $colSpec->{type};
+	push @colWriteType, $colSpec->{writetype};
+    }
+    my $numRows;
+    $inFits->get_num_rows($numRows, $status) and check_fitsio($status);
+
+    my $correct_error = 0;
+#    print STDERR "Ncols:" .  $#{ $column_defs } . "\n";
+    foreach my $col (@$column_defs) {
+	my ($col_num,$col_type,$col_data);
+	$inFits->get_colnum(0, $col->{name}, $col_num, $status) and check_fitsio($status);
+	if ($status != 0) {
+	    $status = 0;
+	    next;
+	}
+	$inFits->get_coltype($col_num, $col_type, undef, undef, $status) and check_fitsio($status);
+	$inFits->read_col($col_type, $col_num, 1, 1, $numRows, 0, $col_data, undef, $status) and check_fitsio($status);
+#	print STDERR "$col\t>>" . $col->{name} . "<<\t>>" . @{ $col_data } . "<<\n";
+	if ($col->{name} eq 'PSF_QF') {
+	    @tmp_Qfact = @{ $col_data };
+	}
+	elsif ($col->{name} eq 'PSF_NPIX') {
+	    @tmp_Npix = @{ $col_data };
+	}
+	elsif ($col->{name} eq 'PSF_INST_MAG') {
+	    @tmp_flux = map { $_ = 10**(-0.4 * $_) } @{ $col_data };
+	}
+	elsif ($col->{name} eq 'PSF_INST_MAG_SIG') {
+	    @tmp_flux_err = map { $_ = $_ / (2.5 * (log(exp(1)) / log(10))) } @{ $col_data };
+	    $correct_error = 1;
+	}
+	elsif ($col->{name} eq 'PSF_INST_FLUX') {
+	    @tmp_flux = @{ $col_data };
+	}
+	elsif ($col->{name} eq 'PSF_INST_FLUX_SIG') {
+	    @tmp_flux_err = @{ $col_data };
+	}
+    }
+    if ($correct_error) {
+	for (my $c = 0; $c <= $#tmp_flux_err; $c++) {
+	    $tmp_flux_err[$c] = $tmp_flux_err[$c] * $tmp_flux[$c];
+	}
+    }
+    $inFits->close_file( $status ) and check_fitsio($status);    
+#    print STDERR "$CMFversion\n";
+#    print STDERR "Q: $#tmp_Qfact\t@tmp_Qfact\nN: $#tmp_Npix\t@tmp_Npix\n";
+    return(\@tmp_Npix, \@tmp_Qfact, \@tmp_flux, \@tmp_flux_err);
+}
+
+# A lot of this pulled verbatim from Bill's detect_response_create file
+sub write_response_file {
+    my $outfile = shift;
+    my $query_ref = shift;
+
+    my %query = %{ $query_ref };
+
+    my $columns;
+    my $headers;
+
+    my $EXTVER_IS_1 = (scalar(keys(%query)) == 1);
+#    print "EXTVER: $EXTVER_IS_1\n";
+    my ($query_id,$FPA_ID,$MJD_OBS,$filter,$obscode,$status);
+    if ($EXTVER_IS_1 == 1) {
+	# Specification of columns to write
+	$columns = [
+	    # matching rownum from detectability original request
+	    { name => 'ROWNUM',   type => '20A', writetype => TSTRING }, 
+	    # any errors that occurred during processing
+	    { name => 'ERROR_CODE',   type => 'V', writetype => TULONG }, 
+	    # number of pixels used in hypothetical PSF for the query detection
+	    { name => 'DETECT_N', type => 'V',   writetype => TULONG },
+	    # detectibility, indicating the fraction of PSF pixels detetable by IPP
+	    { name => 'DETECT_F', type => 'D',   writetype => TDOUBLE },
+	    # flux of the target source
+	    { name => 'TARGET_FLUX', type => 'D', writetype => TDOUBLE },
+	    # error in the flux of the target source
+	    { name => 'TARGET_FLUX_SIG', type => 'D', writetype => TDOUBLE },
+	    ];
+	
+	# Header translation table
+	$headers = {
+	    'QUERY_ID' => { name => 'QUERY_ID', type => TSTRING, 
+			    comment => 'MOPS Query ID for this batch query' },
+	    'FPA_ID' => { name => 'FPA_ID',   type => TSTRING, 
+			  comment => 'original FPA_ID used at ingest' },
+# 	    'MJD-OBS' => { name => 'FPA_ID',   type => TSTRING, 
+# 			  comment => 'original FPA_ID used at ingest' },
+# 	    'FILTER' => { name => 'FPA_ID',   type => TSTRING, 
+# 			  comment => 'original FPA_ID used at ingest' },
+# 	    'OBSCODE' => { name => 'FPA_ID',   type => TSTRING, 
+# 			  comment => 'original FPA_ID used at ingest' },
+	};
+    }
+    else {
+	# Specification of columns to write
+	$columns = [
+	    # matching rownum from detectability original request
+	    { name => 'ROWNUM',   type => '20A', writetype => TSTRING}, 
+	    # any errors that occurred during processing
+	    { name => 'ERROR_CODE',   type => 'V', writetype => TULONG }, 
+	    # number of pixels used in hypothetical PSF for the query detection
+	    { name => 'DETECT_N', type => 'V',   writetype => TULONG },
+	    # detectibility, indicating the fraction of PSF pixels detetable by IPP
+	    { name => 'DETECT_F', type => 'D',   writetype => TDOUBLE },
+	    # flux of the target source
+	    { name => 'TARGET_FLUX', type => 'D', writetype => TDOUBLE },
+	    # error in the flux of the target source
+	    { name => 'TARGET_FLUX_SIG', type => 'D', writetype => TDOUBLE },
+	    # The FPA That would be in the header if it were to be there.
+	    { name => 'FPA_ID',   type => '20A', writetype => TSTRING },
+	    ];
+	
+	# Header translation table
+	$headers = {
+	    'QUERY_ID' => { name => 'QUERY_ID', type => TSTRING, 
+			    comment => 'MOPS Query ID for this batch query' },
+	};
+    }	
+
+    # Parse the list of columns
+    my @colNames;                   # Names of columns
+    my @colTypes;                   # Types of columns
+    my %colData;                    # Data for each column
+    my @colWriteType;                 # type to use to write
+    foreach my $colSpec ( @$columns) {
+	push @colNames, $colSpec->{name};
+	push @colTypes, $colSpec->{type};
+	push @colWriteType, $colSpec->{writetype};
+	$colData{$colSpec->{name}} = [];
+    }
+
+    my $inHeader = { };
+
+    # Hack to force the data to match the detect_response_create formats
+
+    $inHeader->{QUERY_ID}->{value} = $query_id;
+    if ($EXTVER_IS_1 == 1) {
+	my $fpa_id = (keys(%query))[0];
+	$inHeader->{FPA_ID}->{value} = $fpa_id;
+    }
+    
+    # Fill the table columns with the data, making sure the flux is defined
+    foreach my $fpa_id (keys %query) {
+	$inHeader->{QUERY_ID}->{value} = $query{$fpa_id}{QUERY_ID}[0];
+	if ($EXTVER_IS_1 == 1) {
+	    my $fpa_id = (keys(%query))[0];
+	    $inHeader->{FPA_ID}->{value} = $fpa_id;
+	}
+
+        my @keysa = keys %{$query{$fpa_id}};
+
+        print "$fpa_id: @keysa\n";
+	
+	push @{$colData{'ROWNUM'}}, @{ $query{$fpa_id}{ROWNUM} };
+	push @{$colData{'ERROR_CODE'}}, @{ $query{$fpa_id}{PROC_ERROR} };
+	push @{$colData{'DETECT_N'}}, @{ $query{$fpa_id}{NPIX} };
+	push @{$colData{'DETECT_F'}}, @{ $query{$fpa_id}{QFACTOR} };
+	push @{$colData{'TARGET_FLUX'}}, @{ $query{$fpa_id}{FLUX} };
+	push @{$colData{'TARGET_FLUX_SIG'}}, @{ $query{$fpa_id}{FLUX_SIG} };
+	if ($EXTVER_IS_1 != 1) {
+	    push @{$colData{'FPA_ID'}}, (map {$fpa_id} @{ $query{$fpa_id}{ROWNUM} });
+	}
+    }
+    my $numRows = 0;
+    # Back to detect_response_create:
+    $status = 0;
+#    print "$output\n";
+    if (-e $output) {
+        unlink $output or die "failed to unlink existing response file $output\n";
+    }
+    my $outFits = Astro::FITS::CFITSIO::create_file( $output, $status );
+    check_fitsio( $status );
+    $outFits->create_img( 16, 0, undef, $status );
+    check_fitsio( $status );
+    my $EXTNAME = 'MOPS_DETECTABILITY_RESPONSE';
+    
+    $outFits->create_tbl( BINARY_TBL(), $numRows, scalar @colNames, \@colNames, \@colTypes, undef, $EXTNAME, $status);
+    check_fitsio( $status );
+
+    # TODO: get the extension version number from somewhere common
+    $outFits->write_key( TINT, 'EXTVER', $EXTVER, 'Version of this Extension', $status );
+    check_fitsio( $status );
+
+    # Write the Extension keywords
+    foreach my $keyword ( keys %$headers ) {
+	my $value = $inHeader->{$keyword}->{value}; # Header keyword value
+	unless (defined $value) {
+	    print "Can't find header keyword $keyword\n";
+	    next;
+	}
+	$value =~ s/\'//g;
+	my $name    = $headers->{$keyword}->{name}; # New name
+	my $type    = $headers->{$keyword}->{type}; # Type
+	my $comment = $headers->{$keyword}->{comment}; # Comment
+	$outFits->write_key( $type, $name, $value, $comment, $status );
+	check_fitsio( $status );
+    }
+
+    for (my $i = 0; $i < scalar @colNames; $i++) {
+	my $colName = $colNames[$i];# Column name
+	my $writeType = $colWriteType[$i];
+	my $numRows = scalar(@{$colData{$colName}});
+	unless(defined($writeType)) {
+	    print "write type undefined for $colName\n";
+	}
+	unless(defined($numRows)) {
+	    print "num Rows undefined for $colName\n";
+	}
+	unless(defined($status)) {
+	    print "status undefined for $colName\n";
+	}
+	unless(defined($colData{$colName})) {
+	    print "col data undefined for $colName\n";
+	}
+	unless(defined($colName)) {
+	    print "column name undefined for $i\n";
+	}
+	print STDERR "$writeType $i $numRows $colName $status @{ $colData{$colName} }\n";
+	$outFits->write_col( $writeType, $i + 1, 1, 1, $numRows, $colData{$colName}, $status );
+	print STDERR "$writeType $i $numRows $colName $status\n";
+	check_fitsio( $status );
+    }
+    $outFits->close_file( $status );
+    return($status);
+}
+
+# From Astro::FITS::CFITSIO demo
+sub check_fitsio
+{
+    my $status = shift;         # Status of FITSIO calls
+
+    if ($status != 0) {
+        my $msg;                # Message to output
+        Astro::FITS::CFITSIO::fits_get_errstatus( $status , $msg );
+	carp("CFITSIO error: $status => $msg");
+        die "CFITSIO error: $msg\n";
+    }
+}
+
+sub check_component {
+    my ($stage, $stage_id, $component, $imagedb) = @_;
+
+    print "Checking status of component $stage $stage_id $component\n";
+
+    my $command;
+    if ($stage eq 'diff') {
+        $command = "difftool -diffskyfile -diff_id $stage_id -skycell_id $component";
+    } elsif ($stage eq 'stack') {
+        $command = "stacktool -sumskyfile -stack_id $stage_id";
+    } elsif ($stage eq 'warp') {
+        $command = "warptool -warped -warp_id $stage_id -skycell_id $component";
+    } elsif ($stage eq 'chip') {
+        $command = "chiptool -processedimfile -chip_id $stage_id -class_id $component";
+    } else {
+        die("check_component not implemented for stage $stage yet");
+    }
+
+    $command .= " -dbname $imagedb";
+
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+	run(command => $command, verbose => $verbose);
+    unless ($success) {
+        my $rc = $error_code >> 8;
+        carp "$command failed $error_code $rc";
+        exit $PS_EXIT_UNKNOWN_ERROR;
+    }
+    my $output = join "", @$stdout_buf;
+    my $metadata = $mdcParser->parse($output);
+    my $results = parse_md_list($metadata);
+    if (scalar @$results != 1) {
+        carp "$command returned too many components: " . scalar @$results;
+        exit $PS_EXIT_UNKNOWN_ERROR;
+    }
+    my $it = $results->[0];
+
+    if ($stage eq 'stack') {
+        $it->{data_state} = $it->{state};
+    }
+
+    my $return_status = 0;
+    if ($it->{quality}) {
+        print "  Bad quality: $it->{quality}\n";
+        $return_status = $PSTAMP_GONE;
+    } elsif ($it->{fault}) {
+        print "  Faulted: $it->{fault}\n";
+        $return_status = $PSTAMP_GONE;
+    } elsif ($it->{data_state} ne 'full') {
+        # XXX does this work for stack?
+        carp "  Faulted: $it->{data_state}\n";
+        $return_status = $PSTAMP_GONE;
+    } else {
+        print "  Component ok.\n";
+    }
+
+    return $return_status;
+}
+
+sub my_die {
+    my $message = shift;
+    my $QUERY_ID = shift;
+    my $FPA_ID = shift;
+    my $MJD_OBS = shift;
+    my $FILTER = shift;
+    my $OBSCODE = shift;
+    my $STAGE = shift;
+
+    my $exit_code = shift;      # Exit code to add
+
+    $exit_code = $PS_EXIT_PROG_ERROR unless defined $exit_code;
+
+    carp("$message : $QUERY_ID $FPA_ID $MJD_OBS $FILTER $OBSCODE $STAGE");
+    exit($exit_code);
+
+}
+
+sub exit_with_failure {
+    my $status = shift;
+    my $message = shift;
+    carp("$message");
+    exit($status);
+}
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/dquery_finish.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/dquery_finish.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/dquery_finish.pl	(revision 41103)
@@ -0,0 +1,400 @@
+#!/bin/env perl
+
+# dquery_finish.pl
+
+use warnings;
+use strict;
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+
+use Sys::Hostname;
+use IPC::Cmd 0.36 qw( can_run run );
+use File::Temp qw( tempfile );
+use File::Copy;
+use Astro::FITS::CFITSIO qw( :constants );
+Astro::FITS::CFITSIO::PerlyUnpacking(1);
+
+use PS::IPP::Metadata::Config;
+use PS::IPP::Metadata::Stats;
+use PS::IPP::Metadata::List qw( parse_md_list );
+
+use PS::IPP::Config qw( :standard );
+
+my ( $req_id, $req_name, $req_file, $product, $outdir, $dbname, $dbserver, $verbose, $save_temps );
+
+GetOptions(
+           'req_id=s'   => \$req_id,
+           'req_name=s' => \$req_name,
+           'req_file=s' => \$req_file,
+           'product=s'  => \$product,
+           'outdir=s'   => \$outdir,
+	   'dbname=s'   => \$dbname,
+	   'dbserver=s' => \$dbserver,
+	   'verbose'    => \$verbose,
+	   'save-temps' => \$save_temps,
+) or pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+
+my $err = "";
+
+$err .= "--req_id is required\n" if !$req_id;
+$err .= "--req_name is required\n" if !$req_name;
+$err .= "--product is required\n" if !$product;
+$err .= "--outdir is required\n" if !$outdir;
+
+die "$err" if $err;
+
+my $missing_tools;
+
+my $pstamptool  = can_run('pstamptool')  or (warn "Can't find pstamptool"  and $missing_tools = 1);
+my $dsreg  = can_run('dsreg')  or (warn "Can't find dsreg"  and $missing_tools = 1);
+
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+my $ipprc = PS::IPP::Config->new(); # IPP Configuration
+
+my $outputDataStoreRoot = metadataLookupStr($ipprc->{_siteConfig}, 'DATA_STORE_ROOT');
+exit ($PS_EXIT_CONFIG_ERROR) unless defined $outputDataStoreRoot; # lookup failure outputs a message
+
+if (!$dbserver) {
+    $dbserver = metadataLookupStr($ipprc->{_siteConfig}, 'PS_DBSERVER');
+}
+
+if ($product eq "NULL") {
+    # parsing failed just with fault = 0 (this leaves previously set fault in place
+    update_request($req_id, 0, $verbose);
+    exit 0;
+}
+
+if (! -e $outdir ) {
+    # something must have gone wrong at the parse stage
+    print STDERR "output fileset directory $outdir does not exist\n" if $verbose;
+    if (! mkdir $outdir ) {
+        update_request($req_id, $PS_EXIT_SYS_ERROR, $verbose);
+        die "cannot create output directory $outdir";
+    }
+} elsif (! -d $outdir) {
+    update_request($req_id, $PS_EXIT_SYS_ERROR, $verbose);
+    die "output fileset directory $outdir exists but is not a directory";
+}
+
+
+my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files
+
+my $request_fault = 0;
+my @jobs;
+{
+    my $command = "$pstamptool -listjob -req_id $req_id";
+    $command .= " -jobType detect_query";   # temporary
+    $command .= " -dbname $dbname" if $dbname;
+    $command .= " -dbserver $dbserver" if $dbserver;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        die("Unable to perform $command error code: $error_code");
+    }
+    my $output = join "", @$stdout_buf;
+    if (!$output) {
+        if ($verbose) {
+            print STDERR "Request $req_id produced no jobs.\n";
+        }
+        # assume that the parser set the fault
+    } else {
+        my $metadata =  $mdcParser->parse($output) or die("Unable to parse metdata config doc for jobs list");
+        my $jobs = parse_md_list($metadata);
+        @jobs = @$jobs;
+    }
+}
+
+my ($REGLIST, $reg_list) = tempfile("$outdir/reglist.XXXX", UNLINK => !$save_temps);
+my $response_file = "response.$req_id.fits";
+my $response_path = "$outdir/$response_file";
+
+print $REGLIST "$response_file|||table|\n";
+
+my @columns = qw(ROWNUM PROC_ERROR NPIX QFACTOR FLUX FLUX_SIG FPA_ID STAGE STAGE_ID COMPONENT CMFFILE);
+my %colData;
+foreach my $col (@columns) {
+    $colData{$col} = [];
+}
+    
+my $i = 0;
+foreach my $job (@jobs) {
+    my $job_id = $job->{job_id};
+
+    if ($job->{fault}) {
+        # how do we get this information back to the user?
+        print STDERR "job: $job_id faulted with $job->{fault}\n";
+        next;
+    }
+    my $outputBase = $job->{outputBase};
+    my $results = "${outputBase}results.txt";
+
+    read_results($results, \%colData);
+
+    my $cmf = $colData{CMFFILE}->[$i];
+    print $REGLIST "$cmf|||table|\n";
+
+    $i++;
+}
+close $REGLIST;
+
+write_response_file($response_path, $req_name, \%colData);
+
+if (-s $reg_list) {
+    my $command = "$dsreg --add $req_name --product $product --list $reg_list";
+    $command .= " --copy --datapath $outdir";
+    $command .= " --type MOPS_DETECTABILITY_RESPONSE";
+    $command .= " --ps0 $req_id";
+    $command .= " --dbname $dbname" if $dbname;
+
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        $request_fault = $error_code >> 8;
+        print STDERR "Unable to perform $command return code: $request_fault";
+        # fall through to call stop request
+    }
+}
+
+update_request($req_id, $request_fault, $verbose);
+
+exit 0;
+
+sub read_results {
+    my $results_file = shift;
+    my $results = shift;
+
+    open IN, "<$results_file" or my_die("failed to open $results_file\n", $PS_EXIT_UNKNOWN_ERROR);
+
+    foreach my $line (<IN>) {
+        # skip header
+        next if $line =~ /ROWNUM/;
+        chomp $line;
+        my @words = split " ", $line;
+        my ($rownum, $proc_error, $npix, $qfactor, $flux, $flux_sig, $fpa_id, $stage, $stage_id, $component, $cmf) = @words;
+        push @{$results->{ROWNUM}}, $rownum;
+        push @{$results->{PROC_ERROR}}, $proc_error;
+        push @{$results->{NPIX}}, $npix;
+        push @{$results->{QFACTOR}}, $qfactor;
+        push @{$results->{FLUX}}, $flux;
+        push @{$results->{FLUX_SIG}}, $flux_sig;
+        push @{$results->{FPA_ID}}, $fpa_id;
+        push @{$results->{STAGE}}, $stage;
+        push @{$results->{STAGE_ID}}, $stage_id;
+        push @{$results->{COMPONENT}}, $component;
+        push @{$results->{CMFFILE}}, $cmf;
+    }
+    # XXX: make sure that the size of each of these arrays is the same
+}
+
+sub write_response_file {
+    my $output = shift;
+    my $query_id = shift;
+    my $results = shift;
+
+    my $columns;
+    my $headers;
+
+    my $EXTVER_IS_1 = 0 ; # (scalar(keys(%query)) == 1);
+#    print "EXTVER: $EXTVER_IS_1\n";
+    my ($FPA_ID,$MJD_OBS,$filter,$obscode,$status);
+    if ($EXTVER_IS_1 == 1) {
+	# Specification of columns to write
+	$columns = [
+	    # matching rownum from detectability original request
+	    { name => 'ROWNUM',   type => '20A', writetype => TSTRING }, 
+	    # any errors that occurred during processing
+	    { name => 'ERROR_CODE',   type => 'V', writetype => TULONG }, 
+	    # number of pixels used in hypothetical PSF for the query detection
+	    { name => 'DETECT_N', type => 'V',   writetype => TULONG },
+	    # detectibility, indicating the fraction of PSF pixels detetable by IPP
+	    { name => 'DETECT_F', type => 'D',   writetype => TDOUBLE },
+	    # flux of the target source
+	    { name => 'TARGET_FLUX', type => 'D', writetype => TDOUBLE },
+	    # error in the flux of the target source
+	    { name => 'TARGET_FLUX_SIG', type => 'D', writetype => TDOUBLE },
+	    ];
+	
+	# Header translation table
+	$headers = {
+	    'QUERY_ID' => { name => 'QUERY_ID', type => TSTRING, 
+			    comment => 'MOPS Query ID for this batch query' },
+	    'FPA_ID' => { name => 'FPA_ID',   type => TSTRING, 
+			  comment => 'original FPA_ID used at ingest' },
+# 	    'MJD-OBS' => { name => 'FPA_ID',   type => TSTRING, 
+# 			  comment => 'original FPA_ID used at ingest' },
+# 	    'FILTER' => { name => 'FPA_ID',   type => TSTRING, 
+# 			  comment => 'original FPA_ID used at ingest' },
+# 	    'OBSCODE' => { name => 'FPA_ID',   type => TSTRING, 
+# 			  comment => 'original FPA_ID used at ingest' },
+	};
+    }
+    else {
+	# Specification of columns to write
+	$columns = [
+	    # matching rownum from detectability original request
+	    { name => 'ROWNUM',   type => '20A', writetype => TSTRING}, 
+	    # any errors that occurred during processing
+	    { name => 'ERROR_CODE',   type => 'V', writetype => TULONG }, 
+	    # number of pixels used in hypothetical PSF for the query detection
+	    { name => 'DETECT_N', type => 'V',   writetype => TULONG },
+	    # detectibility, indicating the fraction of PSF pixels detetable by IPP
+	    { name => 'DETECT_F', type => 'D',   writetype => TDOUBLE },
+	    # flux of the target source
+	    { name => 'TARGET_FLUX', type => 'D', writetype => TDOUBLE },
+	    # error in the flux of the target source
+	    { name => 'TARGET_FLUX_SIG', type => 'D', writetype => TDOUBLE },
+	    # The FPA That would be in the header if it were to be there.
+	    { name => 'FPA_ID',   type => '20A', writetype => TSTRING },
+	    ];
+	
+	# Header translation table
+	$headers = {
+	    'QUERY_ID' => { name => 'QUERY_ID', type => TSTRING, 
+			    comment => 'MOPS Query ID for this batch query' },
+	};
+    }	
+
+    # Parse the list of columns
+    my @colNames;                   # Names of columns
+    my @colTypes;                   # Types of columns
+    my %colData;                    # Data for each column
+    my @colWriteType;                 # type to use to write
+    foreach my $colSpec ( @$columns) {
+	push @colNames, $colSpec->{name};
+	push @colTypes, $colSpec->{type};
+	push @colWriteType, $colSpec->{writetype};
+	$colData{$colSpec->{name}} = [];
+    }
+
+    my $inHeader = { };
+
+    # Hack to force the data to match the detect_response_create formats
+
+    $inHeader->{QUERY_ID}->{value} = $query_id;
+    if ($EXTVER_IS_1 == 1) {
+	my $fpa_id = $results->{FPA_ID}->[0];
+	$inHeader->{FPA_ID}->{value} = $fpa_id;
+    }
+    
+    # Fill the table columns with the data, making sure the flux is defined
+
+    @{$colData{'ROWNUM'}} = @{ $results->{ROWNUM} };
+    @{$colData{'ERROR_CODE'}} = @{ $results->{PROC_ERROR} };
+    @{$colData{'DETECT_N'}} = @{ $results->{NPIX} };
+    @{$colData{'DETECT_F'}} = @{ $results->{QFACTOR} };
+    @{$colData{'TARGET_FLUX'}} = @{ $results->{FLUX} };
+    @{$colData{'TARGET_FLUX_SIG'}} = @{ $results->{FLUX_SIG} };
+    @{$colData{'FPA_ID'}} = @{ $results->{FPA_ID} };
+
+    my $numRows = 0;
+    # Back to detect_response_create:
+    $status = 0;
+#    print "$output\n";
+    if (-e $output) {
+        unlink $output or die "failed to unlink existing response file $output\n";
+    }
+    my $outFits = Astro::FITS::CFITSIO::create_file( $output, $status );
+    check_fitsio( $status );
+    $outFits->create_img( 16, 0, undef, $status );
+    check_fitsio( $status );
+    my $EXTNAME = 'MOPS_DETECTABILITY_RESPONSE';
+    
+    $outFits->create_tbl( BINARY_TBL(), $numRows, scalar @colNames, \@colNames, \@colTypes, undef, $EXTNAME, $status);
+    check_fitsio( $status );
+
+    # TODO: get the extension version number from somewhere common
+    $outFits->write_key( TINT, 'EXTVER', 2, 'Version of this Extension', $status );
+    check_fitsio( $status );
+
+    # Write the Extension keywords
+    foreach my $keyword ( keys %$headers ) {
+	my $value = $inHeader->{$keyword}->{value}; # Header keyword value
+	unless (defined $value) {
+	    print "Can't find header keyword $keyword\n";
+	    next;
+	}
+	$value =~ s/\'//g;
+	my $name    = $headers->{$keyword}->{name}; # New name
+	my $type    = $headers->{$keyword}->{type}; # Type
+	my $comment = $headers->{$keyword}->{comment}; # Comment
+	$outFits->write_key( $type, $name, $value, $comment, $status );
+	check_fitsio( $status );
+    }
+
+    for (my $i = 0; $i < scalar @colNames; $i++) {
+	my $colName = $colNames[$i];# Column name
+	my $writeType = $colWriteType[$i];
+	my $numRows = scalar(@{$colData{$colName}});
+	unless(defined($writeType)) {
+	    print "write type undefined for $colName\n";
+	}
+	unless(defined($numRows)) {
+	    print "num Rows undefined for $colName\n";
+	}
+	unless(defined($status)) {
+	    print "status undefined for $colName\n";
+	}
+	unless(defined($colData{$colName})) {
+	    print "col data undefined for $colName\n";
+	}
+	unless(defined($colName)) {
+	    print "column name undefined for $i\n";
+	}
+	print STDERR "$writeType $i $numRows $colName $status @{ $colData{$colName} }\n";
+	$outFits->write_col( $writeType, $i + 1, 1, 1, $numRows, $colData{$colName}, $status );
+	print STDERR "$writeType $i $numRows $colName $status\n";
+	check_fitsio( $status );
+    }
+    $outFits->close_file( $status );
+    return($status);
+}
+
+# From Astro::FITS::CFITSIO demo
+sub check_fitsio
+{
+    my $status = shift;         # Status of FITSIO calls
+
+    if ($status != 0) {
+        my $msg;                # Message to output
+        Astro::FITS::CFITSIO::fits_get_errstatus( $status , $msg );
+	carp("CFITSIO error: $status => $msg");
+        die "CFITSIO error: $msg\n";
+    }
+}
+sub update_request {
+    my $req_id = shift;
+    my $fault = shift;
+    my $verbose = shift;
+    
+    my $command = "$pstamptool -updatereq -req_id $req_id";
+    if ($fault) {
+        $command .= " -set_fault $fault";
+    } else {
+        $command .= " -set_state stop";
+    }
+    $command   .= " -dbname $dbname" if $dbname;
+    $command   .= " -dbserver $dbserver" if $dbserver;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        die("Unable to perform $command error code: $error_code");
+    }
+}
+
+sub my_die {
+    my $msg = shift;
+    my $fault = shift;
+
+    print STDERR $msg;
+
+    update_request($req_id, $fault);
+
+    exit $fault;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/dquery_job_run.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/dquery_job_run.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/dquery_job_run.pl	(revision 41103)
@@ -0,0 +1,400 @@
+#! /usr/bin/env perl
+#
+#
+# Run a detectability query job
+#
+#
+
+use strict;
+use warnings;
+
+use Sys::Hostname;
+use Carp;
+use File::Basename;
+use File::Copy;
+use IPC::Cmd 0.36 qw( can_run run );
+use Getopt::Long qw( GetOptions );
+use Pod::Usage qw( pod2usage );
+use File::Temp qw( tempfile tempdir);
+
+use PS::IPP::PStamp::RequestFile qw( :standard );
+use PS::IPP::PStamp::Job qw( :standard );
+use PS::IPP::Config qw( :standard );
+use PS::IPP::Metadata::List qw( parse_md_list );
+
+use Astro::FITS::CFITSIO qw( :constants );
+Astro::FITS::CFITSIO::PerlyUnpacking(1);
+
+#
+# Set up
+#
+my $host = hostname();
+
+my $EXTVER = 1.0;
+my $EXTNAME = 'MOPS_DETECTABILITY_RESPONSE';
+my ($req_id,$job_id,$req_name,$product,$need_magic,$missing_tools,$project);
+my ($output_base,$dbname,$dbserver,$verbose,$save_temps);
+GetOptions(
+    'output_base=s'   =>      \$output_base,
+    'job_id=s'        =>      \$job_id,
+    'dbname=s'        =>      \$dbname,
+    'dbserver=s'      =>      \$dbserver,
+    'verbose'         =>      \$verbose,
+    'save-temps'      =>      \$save_temps,
+    ) or pod2usage(2);
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --job_id --output_base",
+	   -exitval => 3,
+    ) unless
+    defined $job_id and defined $output_base;
+
+my $psphotForced      = can_run('psphotForced') or (warn "Can't find psphotForced" and $missing_tools = 1);
+my $ppCoord           = can_run('ppCoord') or (warn "Can't find ppCoord" and $missing_tools = 1);
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit($PS_EXIT_CONFIG_ERROR);
+}
+
+my $workdir = dirname($output_base);
+my $params_file = "${output_base}params.mdc";
+
+unless (defined $verbose) {
+    $verbose = 0;
+}
+
+my $ipprc = PS::IPP::Config->new();
+my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files
+
+if (!$dbserver) {
+    $dbserver = metadataLookupStr($ipprc->{_siteConfig}, 'PS_DBSERVER');
+}
+
+my $imagedb = 'gpc1';
+my %args;
+
+open IN, "<$params_file" 
+    or my_die("failed to open params file: $params_file", "", "", "", "", "", "", $PS_EXIT_UNKNOWN_ERROR);
+
+my $data = $mdcParser->parse(join "", (<IN>))
+    or my_die("failed to parse paramsfile: $params_file", "", "", "", "", "", "", $PS_EXIT_UNKNOWN_ERROR);
+my $refs =  parse_md_list($data);
+if (scalar @$refs < 2) {
+    my_die("invalid paramsfile: $params_file", "", "", "", "", "", "", $PS_EXIT_UNKNOWN_ERROR);
+}
+
+my $params = shift @$refs;
+my @rows = @$refs;
+
+my $nRows = scalar @rows;
+print "Read $nRows rows from $params_file\n";
+    
+#exit 0;
+
+# run ppCoord and psphotForced to calculate the required data.
+my $fpa_id = $params->{FPA_ID};
+my $fault = $params->{FAULT};
+my $catalog = $params->{CATALOG};
+my $psf   = $params->{PSF};
+my $image  = $params->{IMAGE};
+my $mask  = $params->{MASK};
+my $weight= $params->{WEIGHT};
+my $stage = $params->{STAGE};
+my $stage_id = $params->{STAGE_ID};
+my $component = $params->{COMPONENT_ID};
+# print "Input is from $stage $stage_id $component\n";
+
+# XXX: TODO: if there's a fault or quality problem, then we can't process this image.
+# Perhaps we shouldn't even get here
+
+# Create coordinate file to convert to positions.
+my ($coordfile,$coordname) = tempfile("${workdir}/detect.coords.$job_id.XXXX", 
+                                      UNLINK => !$save_temps);
+my ($targetfile,$targetname) = tempfile("${workdir}/detect.targets.$job_id.XXXX", 
+                                        UNLINK => !$save_temps);
+foreach my $row (@rows) {
+    print $coordfile "$row->{RA1_DEG} $row->{DEC1_DEG}\n";
+}
+
+# Convert the sky coordinates to image coordinates with ppCoord.
+my $command = "ppCoord -astrom $catalog -radec $coordname";
+my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+    run(command => $command, verbose => $verbose);
+unless ($success) {
+    my_die("Unable to perform $command. Error_code: $error_code",
+           $params->{QUERY_ID},$fpa_id,$params->{'MJD-OBS'},
+           $params->{FILTER},$params->{OBSCODE},$params->{STAGE},
+           $error_code);
+}
+my @response = split /\n/, (join "", @$stdout_buf);
+my $i = 0;
+foreach my $line (@response) {
+    my ($r_ra,$r_dec,$trash,$r_x,$r_y,$r_chip) = split /\s+/, $line;
+    print $targetfile "$r_x $r_y\n";
+    my $row = $rows[$i];
+    if ((abs($r_ra - $row->{RA1_DEG}) < 1e-8) && (abs($r_dec - $row->{DEC1_DEG}) < 1e-8)) {
+        $row->{X_PXL} = $r_x;
+        $row->{Y_PXL} = $r_y;
+        $row->{EXTENSION_BASE} = $r_chip;
+    } else {
+        $error_code = $PS_EXIT_PROG_ERROR;
+        my_die("Unable to match input RA/DEC with output RA/DEC: ($row->{RA1_DEG},$row->{DEC1_DEG}) -> ($r_ra,$r_dec) $error_code",
+           $params->{QUERY_ID},$fpa_id,$params->{'MJD-OBS'},
+           $params->{FILTER},$params->{OBSCODE},$params->{STAGE},
+           $error_code);
+    }
+    $i++;
+}
+
+# Run psphotForced on the target list.
+my $outroot = "${output_base}detectability.${stage}.${stage_id}";
+$outroot .= ".$component" unless $stage eq 'chip';
+
+$params->{PROC_ERROR} = 0;
+my $psphot_cmd = "$psphotForced -psf $psf ";
+$psphot_cmd .= "-file $image ";
+$psphot_cmd .= "-mask $mask ";
+$psphot_cmd .= "-variance $weight ";
+$psphot_cmd .= "-srctext $targetname $outroot ";
+$psphot_cmd .= "-D OUTPUT.FORMAT PS1_DV1 ";
+
+( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+    run(command => $psphot_cmd, verbose => $verbose);
+unless ($success) {
+    # $params->{PROC_ERROR} = $PSTAMP_SYSTEM_ERROR;
+    my $rc = $error_code >> 8;
+    my_die("$command failed: $error_code $rc",
+           $params->{QUERY_ID},$fpa_id,$params->{'MJD-OBS'},
+           $params->{FILTER},$params->{OBSCODE},$params->{STAGE},
+           $rc);
+}
+
+my $results = "${output_base}results.txt";
+open OUT, ">$results" or 
+        my_die("Failed to open results file $results",
+           $params->{QUERY_ID},$fpa_id,$params->{'MJD-OBS'},
+           $params->{FILTER},$params->{OBSCODE},$params->{STAGE},
+           $PS_EXIT_UNKNOWN_ERROR);
+
+# Read the output cmf and produce the data we need for the response file
+my $class_id = $params->{CLASS_ID};
+my $cmf = "${outroot}.${class_id}.cmf";
+my $cmfbase = basename($cmf);
+my ($tmp_Npix,$tmp_Qfact,$tmp_flux,$tmp_flux_error) = read_cmf_file($cmf,$rows[0]->{EXTENSION_BASE});
+$i = 0;
+print OUT "ROWNUM PROC_ERROR NPIX QFACTOR FLUX FLUX_SIG FPA_ID STAGE STAGE_ID COMPONENT CMFFILE\n";
+foreach my $row (@rows) {
+    $row->{PROC_ERROR} = $params->{PROC_ERROR};
+
+    $row->{NPIX} = ${ $tmp_Npix }[$i];
+    $row->{QFACTOR} = ${ $tmp_Qfact }[$i];
+    $row->{FLUX} = ${ $tmp_flux }[$i];
+    $row->{FLUX_SIG} = ${ $tmp_flux_error }[$i];
+    
+    print OUT "$row->{ROWNUM} $row->{PROC_ERROR} $row->{NPIX} $row->{QFACTOR} $row->{FLUX} $row->{FLUX_SIG} $fpa_id $stage $stage_id $component $cmfbase\n";
+    $i++;
+}
+close(OUT);
+
+exit 0;
+
+#
+# Utilities
+#
+
+# Taken largely from detect_query_read
+sub read_cmf_file {
+    my $cmf_file = shift;
+    my $class_id = shift;
+    my $extname = $class_id . ".psf";
+
+    my @tmp_Npix = ();
+    my @tmp_Qfact = ();
+    my @tmp_flux = ();
+    my @tmp_flux_err = ();
+
+    my ($detect_F_col,$detect_N_col,$detect_flux_col,$detect_mag_col) = (-1, -1, -1, -1);
+
+    
+    my $status = 0;
+    my $inFits = Astro::FITS::CFITSIO::open_file( $cmf_file, READONLY, $status ); # Open CMF
+    check_fitsio($status);
+    $inFits->movnam_hdu(BINARY_TBL, $extname, 0, $status) and check_fitsio($status);
+
+    my $inHeader = $inFits->read_header();
+
+    my $CMFversion = $inHeader->{EXTTYPE};
+    $CMFversion =~ s/\'//g;
+    $CMFversion =~ s/\s+//g;
+
+    # This is the only data we actually care about
+    my $column_defs;
+    if ($CMFversion eq 'PS1_V2') {
+	$column_defs = [
+	    { name => 'PSF_QF',   type => '1E', writetype => TDOUBLE },
+	    { name => 'PSF_NPIX', type => '1J', writetype => TLONG },
+	    { name => 'PSF_INST_MAG', type => '1E', writetype => TDOUBLE },
+	    { name => 'PSF_INST_MAG_SIG', type => '1E', writetype => TDOUBLE },
+	    ];
+    }
+    elsif ($CMFversion eq 'PS1_DV1') {
+	$column_defs = [
+	    { name => 'PSF_QF',   type => '1E', writetype => TDOUBLE },
+	    { name => 'PSF_NPIX', type => '1J', writetype => TLONG },
+	    { name => 'PSF_INST_FLUX', type => '1E', writetype => TDOUBLE },
+	    { name => 'PSF_INST_FLUX_SIG', type => '1E', writetype => TDOUBLE },
+	    ];
+    }
+
+    my @colNames; 
+    my @colTypes;
+    my @colWriteType;
+    my %colData;
+    foreach my $colSpec (@$column_defs) {
+	push @colNames, $colSpec->{name};
+	push @colTypes, $colSpec->{type};
+	push @colWriteType, $colSpec->{writetype};
+    }
+    my $numRows;
+    $inFits->get_num_rows($numRows, $status) and check_fitsio($status);
+
+    my $correct_error = 0;
+#    print STDERR "Ncols:" .  $#{ $column_defs } . "\n";
+    foreach my $col (@$column_defs) {
+	my ($col_num,$col_type,$col_data);
+	$inFits->get_colnum(0, $col->{name}, $col_num, $status) and check_fitsio($status);
+	if ($status != 0) {
+	    $status = 0;
+	    next;
+	}
+	$inFits->get_coltype($col_num, $col_type, undef, undef, $status) and check_fitsio($status);
+	$inFits->read_col($col_type, $col_num, 1, 1, $numRows, 0, $col_data, undef, $status) and check_fitsio($status);
+#	print STDERR "$col\t>>" . $col->{name} . "<<\t>>" . @{ $col_data } . "<<\n";
+	if ($col->{name} eq 'PSF_QF') {
+	    @tmp_Qfact = @{ $col_data };
+	}
+	elsif ($col->{name} eq 'PSF_NPIX') {
+	    @tmp_Npix = @{ $col_data };
+	}
+	elsif ($col->{name} eq 'PSF_INST_MAG') {
+	    @tmp_flux = map { $_ = 10**(-0.4 * $_) } @{ $col_data };
+	}
+	elsif ($col->{name} eq 'PSF_INST_MAG_SIG') {
+	    @tmp_flux_err = map { $_ = $_ / (2.5 * (log(exp(1)) / log(10))) } @{ $col_data };
+	    $correct_error = 1;
+	}
+	elsif ($col->{name} eq 'PSF_INST_FLUX') {
+	    @tmp_flux = @{ $col_data };
+	}
+	elsif ($col->{name} eq 'PSF_INST_FLUX_SIG') {
+	    @tmp_flux_err = @{ $col_data };
+	}
+    }
+    if ($correct_error) {
+	for (my $c = 0; $c <= $#tmp_flux_err; $c++) {
+	    $tmp_flux_err[$c] = $tmp_flux_err[$c] * $tmp_flux[$c];
+	}
+    }
+    $inFits->close_file( $status ) and check_fitsio($status);    
+#    print STDERR "$CMFversion\n";
+#    print STDERR "Q: $#tmp_Qfact\t@tmp_Qfact\nN: $#tmp_Npix\t@tmp_Npix\n";
+    return(\@tmp_Npix, \@tmp_Qfact, \@tmp_flux, \@tmp_flux_err);
+}
+
+
+# From Astro::FITS::CFITSIO demo
+sub check_fitsio
+{
+    my $status = shift;         # Status of FITSIO calls
+
+    if ($status != 0) {
+        my $msg;                # Message to output
+        Astro::FITS::CFITSIO::fits_get_errstatus( $status , $msg );
+	carp("CFITSIO error: $status => $msg");
+        die "CFITSIO error: $msg\n";
+    }
+}
+
+sub check_component {
+    my ($stage, $stage_id, $component, $imagedb) = @_;
+
+    print "Checking status of component $stage $stage_id $component\n";
+
+    my $command;
+    if ($stage eq 'diff') {
+        $command = "difftool -diffskyfile -diff_id $stage_id -skycell_id $component";
+    } elsif ($stage eq 'stack') {
+        $command = "stacktool -sumskyfile -stack_id $stage_id";
+    } elsif ($stage eq 'warp') {
+        $command = "warptool -warped -warp_id $stage_id -skycell_id $component";
+    } elsif ($stage eq 'chip') {
+        $command = "chiptool -processedimfile -chip_id $stage_id -class_id $component";
+    } else {
+        die("check_component not implemented for stage $stage yet");
+    }
+
+    $command .= " -dbname $imagedb";
+
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+	run(command => $command, verbose => $verbose);
+    unless ($success) {
+        my $rc = $error_code >> 8;
+        carp "$command failed $error_code $rc";
+        exit $PS_EXIT_UNKNOWN_ERROR;
+    }
+    my $output = join "", @$stdout_buf;
+    my $metadata = $mdcParser->parse($output);
+    my $results = parse_md_list($metadata);
+    if (scalar @$results != 1) {
+        carp "$command returned too many components: " . scalar @$results;
+        exit $PS_EXIT_UNKNOWN_ERROR;
+    }
+    my $it = $results->[0];
+
+    if ($stage eq 'stack') {
+        $it->{data_state} = $it->{state};
+    }
+
+    my $return_status = 0;
+    if ($it->{quality}) {
+        print "  Bad quality: $it->{quality}\n";
+        $return_status = $PSTAMP_GONE;
+    } elsif ($it->{fault}) {
+        print "  Faulted: $it->{fault}\n";
+        $return_status = $PSTAMP_GONE;
+    } elsif ($it->{data_state} ne 'full') {
+        # XXX does this work for stack?
+        carp "  Faulted: $it->{data_state}\n";
+        $return_status = $PSTAMP_GONE;
+    } else {
+        print "  Component ok.\n";
+    }
+
+    return $return_status;
+}
+
+sub my_die {
+    my $message = shift;
+    my $QUERY_ID = shift;
+    my $FPA_ID = shift;
+    my $MJD_OBS = shift;
+    my $FILTER = shift;
+    my $OBSCODE = shift;
+    my $STAGE = shift;
+
+    my $exit_code = shift;      # Exit code to add
+
+    $exit_code = $PS_EXIT_PROG_ERROR unless defined $exit_code;
+
+    carp("$message : $QUERY_ID $FPA_ID $MJD_OBS $FILTER $OBSCODE $STAGE");
+    exit($exit_code);
+
+}
+
+sub exit_with_failure {
+    my $status = shift;
+    my $message = shift;
+    carp("$message");
+    exit($status);
+}
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/dqueryparse.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/dqueryparse.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/dqueryparse.pl	(revision 41103)
@@ -0,0 +1,553 @@
+#! /usr/bin/env perl
+#
+#
+# Create a response to a MOPS_DETECTABILITY_QUERY
+#
+#
+
+use strict;
+use warnings;
+
+use Sys::Hostname;
+use Carp;
+use File::Basename;
+use File::Copy;
+use IPC::Cmd 0.36 qw( can_run run );
+use Getopt::Long qw( GetOptions );
+use Pod::Usage qw( pod2usage );
+use File::Temp qw( tempfile tempdir);
+use POSIX;  # for strftime
+
+use PS::IPP::PStamp::RequestFile qw( :standard );
+use PS::IPP::PStamp::Job qw( :standard );
+use PS::IPP::Config qw( :standard );
+use PS::IPP::Metadata::List qw( parse_md_list );
+
+# use Astro::FITS::CFITSIO qw( :constants );
+# Astro::FITS::CFITSIO::PerlyUnpacking(1);
+
+my $host = hostname();
+
+my $mode = 'queue_job';
+
+my ($req_id, $product, $need_magic, $missing_tools, $project, $label);
+my ($request_file, $outdir, $dbname, $dbserver, $verbose, $save_temps, $no_update);
+GetOptions(
+    'file=s'          =>      \$request_file,
+    'req_id=s'        =>      \$req_id,
+    'outdir=s'        =>      \$outdir,
+    'label=s'         =>      \$label,
+    'product=s'       =>      \$product,
+    'mode=s'          =>      \$mode,
+    'dbname=s'        =>      \$dbname,
+    'dbserver=s'      =>      \$dbserver,
+    'verbose'         =>      \$verbose,
+    'save-temps'      =>      \$save_temps,
+    'no-update'       =>      \$no_update,
+    ) or pod2usage(2);
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --file --req_id --outdir --dbname",
+	   -exitval => 3,
+    ) unless
+    defined $request_file and defined $outdir and defined $dbname and defined $req_id;
+
+my $detect_query_read = can_run('detect_query_read') or (warn "Can't find detect_query_read" and $missing_tools = 1);
+my $pstamptool = can_run('pstamptool') or (warn "Can't find pstamptool" and $missing_tools = 1);
+my $fields = can_run('fields') or (warn "Can't find fields" and $missing_tools = 1);
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit($PS_EXIT_CONFIG_ERROR);
+}
+
+unless (defined $verbose) {
+    $verbose = 0;
+}
+
+my $ipprc = PS::IPP::Config->new();
+my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files
+
+
+if (!$dbserver) {
+    $dbserver = metadataLookupStr($ipprc->{_siteConfig}, 'PS_DBSERVER');
+}
+
+$pstamptool .= " -dbname $dbname -dbserver $dbserver";
+
+# Project name is hardcoded to gpc1 for the moment.
+$project = resolve_project($ipprc,"gpc1",$dbname,$dbserver);
+my $imagedb = $project->{dbname};
+if (!$imagedb) {
+    my_die("failed to find imagedb for project: $project", $PS_EXIT_CONFIG_ERROR);
+}
+
+# get the query id and check the extname and version from the header
+my $fields_output; 
+{
+    my $command = "echo $request_file | $fields -x 0 EXTNAME EXTVER QUERY_ID";
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    
+    $fields_output = join "", @$stdout_buf;
+}
+my (undef, $extname, $extver, $req_name) = split " ", $fields_output;
+
+my_die("$request_file is missing one of EXTNAME EXTVER or QUERY_ID", $PS_EXIT_PROG_ERROR)
+    if !(defined($extname) and defined($extver) and defined($req_name));
+my_die("$request_file has EXTNAME $extname not MOPS_DETECTABILITY_QUERY table", $PS_EXIT_PROG_ERROR)
+    if $extname ne "MOPS_DETECTABILITY_QUERY";
+my_die("$request_file is version $extver expecting 1 or 2", $PS_EXIT_PROG_ERROR)
+    if ($extver ne 1) and ($extver ne 2);
+
+if ($req_id) {
+    my $command = "$pstamptool -listreq  -name $req_name -not_req_id $req_id";
+    # set verbose to false so that error message about request not found doesn't appear in parse_error.txt
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => 0);
+    if ($success) {
+        # -listreq succeeded duplicate request name
+        print STDERR "QUERY_ID $req_name has already been used\n";
+        $command = "$pstamptool -addjob -job_type none -rownum 0 -state stop -fault $PSTAMP_DUP_REQUEST -req_id $req_id";
+        unless ($no_update) {
+            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                run(command => $command, verbose => $verbose);
+            if (!$success) {
+                my_die("$command failed", $PS_EXIT_UNKNOWN_ERROR);
+            }
+        } else {
+            print STDERR "skipping $command\n";
+        }
+        
+        my $datestr = strftime "%Y%m%d%H%M%S.$req_id", gmtime;
+        $req_name = "ERROR.$datestr";
+    }
+    $command = "$pstamptool -updatereq -req_id $req_id  -set_name $req_name -set_outProduct $product";
+    unless ($no_update) {
+        ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+        unless ($success) {
+            my_die("$command failed", $PS_EXIT_UNKNOWN_ERROR);
+        }
+    } else {
+        print STDERR "skipping $command\n";
+    }
+}
+
+
+
+#
+# query is a hash which uses $fpa_id as the key
+# The values are hash references which have the various parameter name as the key
+#   The values of the parameter hashes are arrays which contain the values for individual rows 
+#   in the detectabilty query request
+my %query = ();
+
+{
+    #
+    # Parse input request file using detect_query_read
+    # result is a simple text file
+    #
+    my $dqr_command = "$detect_query_read --dbname $imagedb --input $request_file";
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+	run(command => $dqr_command, verbose => $verbose);
+    unless ($success) {
+	# This is a problem, because I'm not sure how we handle a failure to read something.
+	# We need to return a $PSTAMP_INVALID_REQUEST, I think, but if we can't read it, 
+	# we can't send that response back.
+	die("Unable to perform $dqr_command error code: $error_code");
+    }
+    my $Nrows = 0;
+    {
+	my @column_names = ();
+        # split output into lines skip until the line which lists the column names is found
+        # Parse subsequent lines
+	foreach my $entry (split /\n/, (join "", @$stdout_buf)) {
+	    if ($entry =~ /^#/) {
+		@column_names = split /\s+/, $entry;
+		shift(@column_names);  # Dump the hash sign.
+	    }
+	    elsif (scalar @column_names) {
+		# ROWNUM RA1_DEG DEC1_DEG RA2_DEG DEC2_DEG MAG QUERY_ID FPA_ID MJD-OBS FILTER OBSCODE STAGE
+		my %row_data;
+		@row_data{@column_names} = (split /\s+/, $entry);
+		for (my $i = 0; $i <= $#column_names; $i++) {
+                    push @{ $query{$row_data{"FPA_ID"}}{$column_names[$i]} }, $row_data{$column_names[$i]};
+                    $Nrows = scalar(keys(%query));
+                    # print "$row_data{'FPA_ID'} $Nrows $i $column_names[$i] $row_data{$column_names[$i]}\n";
+                }
+	    }
+	}
+    }
+    #
+    # Identify target components.  This should properly collate targets on a single imfile.
+    #
+    my $query_id;
+    foreach my $fpa_id (keys %query) {
+	my %temp_hash;
+	my $query_style = 'byexp';
+	my $stage;
+	my $filter;
+	my $mjd;
+	# Confirm that we only have one stage/filter/mjd
+	if ($fpa_id ne 'Not_Set') {   # We only need to check things that aren't the known odd case.
+	    for (my $i = 0; $i <= $#{ $query{$fpa_id}{STAGE} }; $i++) {
+		$temp_hash{STAGE}{$query{$fpa_id}{STAGE}[$i]} = 1;
+		$temp_hash{FILTER}{$query{$fpa_id}{FILTER}[$i]} = 1;
+		$temp_hash{'MJD-OBS'}{$query{$fpa_id}{'MJD-OBS'}[$i]} = 1;
+	    }
+	    if (scalar(keys(%{ $temp_hash{STAGE} })) == 1) {
+		$stage = (keys(%{ $temp_hash{STAGE} }))[0];
+	    }
+	    else {
+		exit_with_failure(21,"Too many STAGEs specified");
+	    }
+	    if (scalar(keys(%{ $temp_hash{FILTER} })) == 1) {
+		$filter = (keys(%{ $temp_hash{FILTER} }))[0];
+	    }
+	    else {
+		exit_with_failure(21,"Too many FILTERs specified");
+	    }
+	    if (scalar(keys(%{ $temp_hash{'MJD-OBS'} })) == 1) {
+		$mjd = (keys(%{ $temp_hash{'MJD-OBS'} }))[0];
+	    }
+	    else {
+		exit_with_failure(21,"Too many MJD-OBS specified");
+	    }
+	}
+
+        ## 20190317 MEH hack to add to row
+        # Set common request components
+        my $option_mask |= 1;
+        $option_mask |= $PSTAMP_SELECT_IMAGE;
+        $option_mask |= $PSTAMP_SELECT_MASK;
+        $option_mask |= $PSTAMP_SELECT_VARIANCE;
+        $option_mask |= $PSTAMP_SELECT_PSF;
+
+	# Set up a rowList with default values
+	my @rowList;
+	for (my $i = 0; $i <= $#{ $query{$fpa_id}{STAGE} }; $i++) {
+	    $rowList[$i]->{CENTER_X} = $query{$fpa_id}{RA1_DEG}[$i];
+	    $rowList[$i]->{CENTER_Y} = $query{$fpa_id}{DEC1_DEG}[$i];
+	    $rowList[$i]->{ID} = $query{$fpa_id}{ROWNUM}[$i];
+	    $rowList[$i]->{COORD_MASK} = 0;
+            # 20190317 MEH hack add to row  
+            $rowList[$i]->{OPTION_MASK} = $option_mask;
+	    # Set default values
+	    $query{$fpa_id}{BAD_COMPONENT}[$i] = 1;
+	    $query{$fpa_id}{IMAGE}[$i] = 'no_image';
+	    $query{$fpa_id}{MASK}[$i] = 'no_mask';
+	    $query{$fpa_id}{WEIGHT}[$i] = 'no_weight';
+	    $query{$fpa_id}{PSF}[$i] = 'no_psf';
+
+	    $query{$fpa_id}{STAGE_ID}[$i] = 'no_id';
+	    $query{$fpa_id}{IMAGE_DB}[$i] = 'no_imdb';
+	    $query{$fpa_id}{NEED_MAGIC}[$i] = 'no_magic';
+	    $query{$fpa_id}{MAGICKED}[$i] = 'no_magic';
+	    $query{$fpa_id}{CATALOG}[$i] = 'no_catalog';
+	    $query{$fpa_id}{COMPONENT_ID}[$i] = 'no_component';
+	    $query{$fpa_id}{CLASS_ID}[$i] = 'no_class';
+
+	    $query{$fpa_id}{STATE}[$i] = 'no_state';
+	    $query{$fpa_id}{DATA_STATE}[$i] = 'no_dstate';
+	    $query{$fpa_id}{FAULT}[$i] = 'no_fault';
+	    $query{$fpa_id}{BURNTOOL_STATE}[$i] = 'no_btstate';
+	}
+
+	# Determine the query style for this fpa_id
+	if ($fpa_id =~ /o.*g.*o/) {
+	    $query_style = 'byexp';
+	}
+	elsif ($fpa_id =~ /\d+/) {
+	    $query_style = 'byid';
+	}
+	elsif ($fpa_id eq 'Not_Set') {
+            # no matching file was found skip
+	    next;
+	}
+	else {
+	    exit_with_failure(21,"Parse error in request file");
+	}
+	# Set common request components
+#	my $option_mask |= 1;
+#	$option_mask |= $PSTAMP_SELECT_IMAGE;
+#	$option_mask |= $PSTAMP_SELECT_MASK;
+#	$option_mask |= $PSTAMP_SELECT_VARIANCE;
+#	$option_mask |= $PSTAMP_SELECT_PSF;
+
+        # magic is dead
+	my $need_magic = 0;
+	if ($stage eq 'stack') {
+	    $need_magic = 0;
+	}
+	my $mjd_min = int $mjd;
+	my $mjd_max = $mjd + 1;
+	
+        my $req_filter;
+        if ($filter ne 'Not_Set') {
+            $req_filter = $filter . '%';
+        }
+	
+	# Call the PStamp code to find the images that contain the target on the given MJD in the specified filter.
+	my $pstamp_images_ref = locate_images($ipprc,$imagedb,
+					  \@rowList,
+					  $query_style,$stage,
+					  $fpa_id,undef,undef,
+					  $option_mask,$need_magic,
+					  $mjd_min,$mjd_max,$req_filter,undef,$verbose);  
+	
+	foreach my $this_image_ref (@{ $pstamp_images_ref }) {
+
+            # loop over the rows that this component matched
+            foreach my $valid_index (@{ $this_image_ref->{row_index} }) {
+                $query{$fpa_id}{IMAGE}[$valid_index] = $this_image_ref->{image};
+                $query{$fpa_id}{MASK}[$valid_index] = $this_image_ref->{mask};
+                $query{$fpa_id}{WEIGHT}[$valid_index] = $this_image_ref->{weight};
+                $query{$fpa_id}{PSF}[$valid_index] = $this_image_ref->{psf};
+                $query{$fpa_id}{STAGE_ID}[$valid_index] = $this_image_ref->{stage_id};
+                $query{$fpa_id}{IMAGE_DB}[$valid_index] = $this_image_ref->{imagedb};
+                $query{$fpa_id}{NEED_MAGIC}[$valid_index] = $need_magic;
+                $query{$fpa_id}{BAD_COMPONENT}[$valid_index] = 0;
+                
+                if (exists($this_image_ref->{astrom})) {
+                    $query{$fpa_id}{CATALOG}[$valid_index] = $this_image_ref->{astrom};
+                }
+                else {
+                    $query{$fpa_id}{CATALOG}[$valid_index] = $this_image_ref->{cmf};
+                }
+                if (exists($this_image_ref->{class_id})) {
+                    $query{$fpa_id}{COMPONENT_ID}[$valid_index] = $this_image_ref->{class_id};
+                    $query{$fpa_id}{CLASS_ID}[$valid_index] = $this_image_ref->{class_id};
+                    
+                }
+                else {
+                    $query{$fpa_id}{COMPONENT_ID}[$valid_index] = $this_image_ref->{skycell_id};
+                    $query{$fpa_id}{CLASS_ID}[$valid_index] = 'fpa';
+                }
+                
+                $query{$fpa_id}{STATE}[$valid_index] = $this_image_ref->{state};
+                if (exists($this_image_ref->{data_state})) {
+                    $query{$fpa_id}{DATA_STATE}[$valid_index] = $this_image_ref->{data_state};
+                }
+                else {
+                    $query{$fpa_id}{DATA_STATE}[$valid_index] = $this_image_ref->{state};
+                }
+                $query{$fpa_id}{FAULT}[$valid_index] = 0;
+		    $query{$fpa_id}{MAGICKED}[$valid_index] = $this_image_ref->{magicked};
+                if ($stage eq 'chip') {
+                    $query{$fpa_id}{BURNTOOL_STATE}[$valid_index] = $this_image_ref->{burntool_state};
+                }
+                
+                # Determine if the data exists.
+                if (($query{$fpa_id}{STATE}[$valid_index] eq 'goto_purged') or 
+                    ($query{$fpa_id}{DATA_STATE}[$valid_index] eq 'purged') or
+                    ($query{$fpa_id}{STATE}[$valid_index] eq 'drop') or 
+                    ($query{$fpa_id}{STATE}[$valid_index] eq 'error_cleaned') or 
+                    ($query{$fpa_id}{STATE}[$valid_index] eq 'goto_scrubbed') or 
+                    ($query{$fpa_id}{DATA_STATE}[$valid_index] eq 'scrubbed')) {
+                    
+                    # image is gone and it's not coming back
+                    $query{$fpa_id}{FAULT}[$valid_index] = $PSTAMP_GONE;
+                }
+                elsif ($need_magic and ($query{$fpa_id}{MAGICKED}[$valid_index] eq 0)) {
+                    $query{$fpa_id}{FAULT}[$valid_index] = $PSTAMP_NOT_DESTREAKED;
+                }
+                elsif (($query{$fpa_id}{DATA_STATE}[$valid_index] ne 'full')) {		      
+                    $query{$fpa_id}{FAULT}[$valid_index] = $PSTAMP_NOT_AVAILABLE;
+                    
+                    # updating stacks isn't implemented
+                    if (($stage eq 'stack')) {
+                        $query{$fpa_id}{FAULT}[$valid_index] = $PSTAMP_NOT_IMPLEMENTED;
+                    }
+                    # updating old burntool data isn't implemented
+                    elsif ($stage eq 'chip') {
+                        if ($query{$fpa_id}{BURNTOOL_STATE}[$valid_index] and 
+                            (abs($query{$fpa_id}{BURNTOOL_STATE}[$valid_index]) < 14)) {
+                            $query{$fpa_id}{FAULT}[$valid_index] = $PSTAMP_NOT_IMPLEMENTED;
+			    }
+                    }
+                } # End determining error faults.
+            }
+        }
+    }
+}
+
+my %update_request;
+my %rows_for_component;
+my %faulted_rows;
+
+# now build a hash using image name as key which contains the list of rows for that image
+foreach my $fpa_id (keys %query) {
+    for (my $i = 0; $i <= $#{ $query{$fpa_id}{ROWNUM} }; $i++) {
+        if ($query{$fpa_id}{BAD_COMPONENT}[$i] == 0) {
+            push @{ $rows_for_component{$fpa_id}{$query{$fpa_id}{IMAGE}[$i]} }, $i;
+        } else {
+            $faulted_rows{$query{$fpa_id}{ROWNUM}[$i]} = $query{$fpa_id}{FAULT}[$i];
+        }
+    }
+}
+
+#
+# Finally build the parameter lists and queue jobs for each component
+#
+
+my $job_num = 1;
+foreach my $fpa_id (keys %rows_for_component) {
+    foreach my $component (keys % {$rows_for_component{$fpa_id} } ) {
+        my $i = $rows_for_component{$fpa_id}{$component}[0];
+        my $dep_id = 0;
+        my $data_state = $query{$fpa_id}{DATA_STATE}[$i];
+        if ($data_state ne 'full') {
+            # Queue this data to be updated
+            my $stage = $query{$fpa_id}{STAGE}[$i];
+            my $stage_id = $query{$fpa_id}{STAGE_ID}[$i];
+            my $component = $query{$fpa_id}{COMPONENT_ID}[$i];
+            print "Need to UPDATE $stage $stage_id $component from $data_state\n";
+            $dep_id = queue_update_run($req_id, $outdir, $label, $data_state, $stage, $stage_id, $component, 0, $imagedb);
+        }
+
+        my $outputBase = "$outdir/$job_num" . "_";
+
+        # params_file is an mdc description containing the parameters for the job
+        my $params_file = $outputBase . "params.mdc";
+
+        open PARAMS, ">$params_file" or my_die("failed to create $params_file", $PS_EXIT_UNKNOWN_ERROR);
+
+        # first the job params
+        print PARAMS "dqueryJobParams METADATA\n";
+        foreach my $key (keys %{ $query{$fpa_id} } ) {
+            my $value = $query{$fpa_id}{$key}[0];
+            print PARAMS "\t$key\t\tSTR\t$value\n";
+        }
+        print PARAMS "END\n\n";
+
+        # then one structure with the coordinates for each row
+        print PARAMS "dqueryCoord MULTI\n";
+
+        my @coordkeys = qw(ROWNUM RA1_DEG DEC1_DEG RA2_DEG DEC2_DEG MAG);
+        foreach my $row ( @{ $rows_for_component{$fpa_id}{$component} }) {
+            print PARAMS "\ndqueryCoord METADATA\n";
+            foreach my $key (@coordkeys) {
+                my $value = $query{$fpa_id}{$key}[$row];
+                print PARAMS "\t$key\t\tSTR\t$value\n";
+            }
+            print PARAMS "END\n";
+        }
+        close(PARAMS);
+
+        # use first rownum for this component as the rownum for the job
+        my $rownum = $query{$fpa_id}{ROWNUM}[$rows_for_component{$fpa_id}{$component}[0]];
+
+        my $command = "$pstamptool -addjob -req_id $req_id -outputBase $outputBase -job_type detect_query -state run -rownum $rownum";
+        $command .= " -dep_id $dep_id" if $dep_id;
+        unless ($no_update) {
+            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                run(command => $command, verbose => $verbose);
+            if ($success) {
+                my $job_id = join "", @$stdout_buf;
+                print "   Queued job: $job_id\n";
+            } else {
+                my $exit_status = $error_code >> 8;
+                $exit_status = $PS_EXIT_UNKNOWN_ERROR unless $exit_status;
+                my_die("Unable to perform $command error code: $error_code", $exit_status);
+            }
+        } else {
+            print STDERR "skipping $command\n";
+        }
+        $job_num++;
+    }
+}
+
+foreach my $rownum (keys %faulted_rows ) {
+    my $outputBase = "$outdir/$job_num" . "_";
+    my $fault = $faulted_rows{$rownum};
+    $fault = $PSTAMP_NO_IMAGE_MATCH if ($fault eq 'no_fault');
+    print STDERR "insert faulted job for $rownum: $fault\n";
+    my $command = "$pstamptool -addjob -req_id $req_id -outputBase $outputBase -job_type detect_query -state stop -rownum $rownum -fault $fault";
+    unless ($no_update) {
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+        if ($success) {
+            my $job_id = join "", @$stdout_buf;
+            print "   Queued job: $job_id\n";
+        } else {
+            my $exit_status = $error_code >> 8;
+            $exit_status = $PS_EXIT_UNKNOWN_ERROR unless $exit_status;
+            my_die("Unable to perform $command error code: $error_code", $exit_status);
+        }
+    } else {
+        print STDERR "skipping $command\n";
+    }
+    $job_num++;
+}
+
+{
+    my $command = "$pstamptool -updatereq -req_id $req_id -set_name $req_name -set_outProduct $product";
+    # $command .= " -set_fault $result" if $result;
+
+    unless ($no_update) {
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+        unless ($success) {
+        my_die("$command failed",$PS_EXIT_UNKNOWN_ERROR);
+        }
+    } else {
+        print STDERR "Skipping $command\n";
+    }
+}
+
+exit 0;
+
+sub queue_update_run {
+    my ($req_id, $outdir, $label, $state, $stage, $stage_id, $component, $need_magic, $imagedb) = @_;
+
+#    my ($state, $stage, $stage_id, $component, $need_magic, $imagedb) = split /\s+/, $data_to_update;
+    
+    if (($state ne 'cleaned') and ($state ne 'update') and ($state ne 'goto_cleaned')) {
+	# We should have received one of these states, so if not, signal that we have a problem.
+	my_die("$stage $stage_id is in unexpected state $state", $PS_EXIT_PROG_ERROR);
+    }
+    my $dep_id;
+    my $command = "$pstamptool -getdependent -stage $stage -stage_id $stage_id -imagedb $imagedb -component $component ";
+    $command .= " -outdir $outdir";
+    $command .= " -need_magic" if $need_magic;
+
+    my $rlabel = "ps_ud_" . $label if $label;
+    $command .= " -rlabel $rlabel" if $rlabel;
+
+    if (!$no_update) {
+	my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+	    run(command => $command, verbose => $verbose);
+	unless ($success) {
+	    my_die("$command failed", $PS_EXIT_UNKNOWN_ERROR);
+	}
+	my $output = join "", @$stdout_buf;
+	chomp $output;
+	$dep_id = $output;
+
+	my_die("pstamptool -getdependent returned invalid dep_id", $PS_EXIT_PROG_ERROR) if !$dep_id;
+    } 
+    else {
+	print STDERR "skipping $command\n";
+	$dep_id = 42;
+    }
+
+    return($dep_id);
+}
+
+
+sub my_die {
+    my $message = shift;
+    my $exit_code = shift;
+
+    $exit_code = $PS_EXIT_PROG_ERROR unless defined $exit_code;
+
+    carp("$message");
+    exit($exit_code);
+}
+
+sub exit_with_failure {
+    my $status = shift;
+    my $message = shift;
+    my_die($message, $status);
+}
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/fakedresponse.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/fakedresponse.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/fakedresponse.pl	(revision 41103)
@@ -0,0 +1,103 @@
+#!/usr/bin/env perl
+#
+# parse a MOPS_DETCTABILITY_QUERY table and create a results file
+#
+# Note: this file is currently only a placeholder which creates a fake response file
+#
+
+use strict;
+use warnings;
+
+use Getopt::Long qw( GetOptions );
+use Pod::Usage qw( pod2usage );
+use DBI;
+use PS::IPP::Metadata::Config;
+use PS::IPP::Metadata::Stats;
+use PS::IPP::Metadata::List qw( parse_md_list );
+use File::Copy;
+use File::Basename;
+
+use PS::IPP::Config qw($PS_EXIT_SUCCESS
+		       $PS_EXIT_UNKNOWN_ERROR
+		       $PS_EXIT_SYS_ERROR
+		       $PS_EXIT_CONFIG_ERROR
+		       $PS_EXIT_PROG_ERROR
+		       $PS_EXIT_DATA_ERROR
+		       $PS_EXIT_TIMEOUT_ERROR
+		       metadataLookupStr
+		       metadataLookupBool
+		       caturi
+		       );
+my ($input, $output, $verbose, $save_temps, $workdir);
+#
+# parse args
+#
+
+
+GetOptions(
+        'input=s'          =>     \$input,
+        'output=s'        =>      \$output,
+        'workdir=s'       =>      \$workdir,
+        'verbose'         =>      \$verbose,
+        'save-temps'      =>      \$save_temps,
+) or pod2usage(2);
+
+my $err = "";
+
+if (!$input) {
+    $err .= "--input is required\n";
+}
+if (!$output) {
+    $err .="--output is required\n";
+}
+
+die $err if ($err);
+
+# spit the contents of the file in text format 
+# XXX Do we lose precision on the floats when we do this?
+
+my $query_text = `detect_query_read --nolabel --input $input`;
+
+my @lines = split "^", $query_text;
+
+# first line is the header keywords
+my $header = shift @lines;
+chomp $header;
+die "failed to parse $input" unless $header;
+
+my ($query_id, $fpa_id, $dateobs, $filter, $obscode) = split " ", $header;
+die "failed to parse $input" unless $query_id && $fpa_id && $dateobs && $filter && $obscode;
+
+if (!$workdir) {
+    $workdir="/tmp";
+}
+my $txt_file = "$workdir/response$$.txt";
+open OUT, ">$txt_file";
+
+print OUT "$query_id $fpa_id $dateobs $filter $obscode\n";
+
+while (my $line = shift @lines) {
+    chomp $line;
+    my ($rownum, $detect_n, $detect_f) = fake_dquery_response($line);
+
+    printf OUT "%-12s %-8d %-8.4f\n", $rownum, $detect_n, $detect_f;
+}
+close OUT;
+
+my $result = system "detect_response_create --input $txt_file --output $output";
+
+unlink $txt_file unless $save_temps;
+
+exit $result;
+
+
+sub fake_dquery_response {
+    my $line = shift;
+    my ($rownum, $ra1, $dec1, $ra2, $dec2, $mag) = split " ", $line;
+
+    # todo perhaps think more about these values
+    my $n = rand(16);
+    my $f = rand(1);
+
+    return ($rownum, $n, $f);
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/ftpsrequest
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/ftpsrequest	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/ftpsrequest	(revision 41103)
@@ -0,0 +1,203 @@
+#!/usr/bin/perl
+
+# ftpsrequest
+
+# Creates a PS1 postage stamp request table using the FTOOLS program ftcreate.
+# The input file format is one line per row with columns described in the array columnsAndTypes given
+# below. 
+#
+# Note: The input format is the same as that used for the rows by the IPP program pstamp_request_file with
+# the exception that the comment is contained in the last column as a quoted string rather than by the |
+# character.
+
+# the option --print-sample may be used to print out an example of a line in an input file to stdout.
+#
+
+# The program ftcreate is part of the FTOOLS package which is described at http://heasarc.gsfc.nasa.gov/ftools
+# and in
+# Blackburn, J. K. 1995, in ASP Conf. Ser., Vol. 77, Astronomical Data Analysis Software and Systems IV, 
+# ed. R. A. Shaw, H. E. Payne, and J. J. E. Hayes (San Francisco: ASP), 367. 
+
+use strict;
+use warnings;
+
+# using a minimal number of perl modules for ease of portablity
+
+use File::Temp qw(tempfile);
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+
+# Array describing the columns in the request table and the order of the columns in the input file
+# third column is value for sample request. A stamp for an i band stack of the tadpole galaxy.
+my @columnsAndTypes = qw(
+        ROWNUM      J   1
+        CENTER_X    D   241.516417
+        CENTER_Y    D   55.425369
+        WIDTH       D   1000
+        HEIGHT      D   1000
+        COORD_MASK  J   2
+        JOB_TYPE    16A stamp
+        OPTION_MASK J   65
+        PROJECT     16A gpc1
+        SURVEY_NAME 16A 3PI
+        IPP_RELEASE 64A null
+        REQ_TYPE    16A bycoord
+        IMG_TYPE    16A stack
+        ID          16A null
+        TESS_ID     64A null
+        COMPONENT   64A null
+        DATA_GROUP  64A null
+        REQFILT     16A i
+        MJD_MIN     D   0
+        MJD_MAX     D   0
+        RUN_TYPE    16A null
+        FWHM_MIN    D   0
+        FWHM_MAX    D   0
+        COMMENT     64A tadpole_galaxy
+);
+
+
+my ( $input,			# Name of input text file
+     $output,			# Name of output table
+     $req_name, 
+     $print_sample,
+     $email, 
+     $clobber,
+     $help,
+     $verbose,
+     $save_temps,
+     );
+
+
+GetOptions(
+	   'input|i=s'      => \$input,
+	   'output|o=s'     => \$output,
+	   'req_name|r=s'   => \$req_name,
+	   'email=s'        => \$email,
+           'help'           => \$help,
+           'print-sample'   => \$print_sample,
+           'clobber'        => \$clobber,
+	   'save-temps'     => \$save_temps,
+	   'verbose'        => \$verbose,
+) or pod2usage( 2 );
+
+my $usageMessage = "$0: --input <input> --output <output> -req_name <request_name>";
+if ($help) {
+    # pod2usage( -msg => $usageMessage, -exitval => 0);
+    pod2usage( -exitval => 0);
+}
+if ($print_sample) {
+    printSample();
+}
+
+pod2usage ( -msg => "Required options: --input --output --req_name\n", -exitval => 1)
+    unless 
+        defined $input and
+        defined $output and
+        defined $req_name;
+
+
+system "which ftcreate >& /dev/null";
+if ($?){
+    die "Can't find required program ftcreate\n";
+}
+
+# temp files for header keyword file and column descriptor file
+my ($hfile, $hfilename) = tempfile( "/tmp/hfile.XXXX", UNLINK => !$save_temps, SUFFIX => '.txt');
+my ($cfile, $cfilename) = tempfile( "/tmp/cfile.XXXX", UNLINK => !$save_temps, SUFFIX => '.txt');
+
+# create the temp files
+makeHeaderFile($hfile, $req_name, $email);
+makeColumnDescriptorFile($cfile);
+
+# here is the command that builds the fits table
+my $cmd = "ftcreate extname=PS1_PS_REQUEST headfile=$hfilename $cfilename $input $output";
+$cmd .= ' clobber=yes' if $clobber;
+
+print STDERR "$cmd\n" if $verbose;
+
+my $rc = system $cmd;
+if ($rc) {
+    my $status = $rc >> 8;
+    die "ftcreate exited with $status ($rc)\n";
+}
+
+exit 0; 
+
+############################
+
+sub makeHeaderFile {
+    my ($hfile, $req_name, $email) = @_;
+
+    $email = 'null' if !defined $email;
+
+    print $hfile "EXTVER=2 / PSTAMP request format version\n";
+    print $hfile "ACTION=PROCESS\n";
+    print $hfile "REQ_NAME=$req_name / unique name for the postage stamp request\n";
+    print $hfile "EMAIL=$email\n";
+    close $hfile;
+}
+
+
+
+sub makeColumnDescriptorFile {
+    my $cfile = shift;
+
+    my $n = scalar @columnsAndTypes;
+
+    die "ERROR: number of columnsAndTypes must be multiple of 3\n" if $n % 3;
+
+    for (my $i = 0; $i < $n; $i += 3) {
+        print $cfile "$columnsAndTypes[$i]\t$columnsAndTypes[$i+1]\n";
+    }
+    close $cfile;
+}
+
+sub printSample {
+    # header line as a comment
+    print "#";
+    for (my $i = 0; $i < scalar @columnsAndTypes; $i += 3) {
+        print " $columnsAndTypes[$i]";
+    }
+    print "\n";
+
+    # sample values
+    my $i;
+    for ($i = 0; $i < scalar @columnsAndTypes - 3; $i += 3) {
+        print " $columnsAndTypes[$i+2]";
+    }
+    # last column is the COMMENT and it needs to be quoted
+    print " '$columnsAndTypes[$i+2]'";
+    print "\n";
+
+    exit 0;
+}
+
+__END__
+
+=pod
+
+=head1 NAME
+
+ftpsrequest - use ftcreate to create a postage stamp request fits table from a textual description
+
+=head1 SYNOPSIS
+
+  ftpsrequest --req_name <request name> --input <input text file> --output <output file name>
+
+    Options:
+        --req_name      REQ_NAME value for the fits header
+        --input         name of ASCII file giving the table values
+        --output        name of output fits table
+        --email         email address of user (optional)
+        --clobber       overwrite existing output file
+        --help          print usage message
+        --print-sample  print out an sample input text file
+
+=head1 NOTE
+
+  In order for ftpsrequest to succeed the ftools program ftcreate must be found in the users path.
+
+  FTOOLS are described at http://heasarc.gsfc.nasa.gov/ftools
+
+=cut
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/psgetcalibinfo
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/psgetcalibinfo	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/psgetcalibinfo	(revision 41103)
@@ -0,0 +1,107 @@
+#!/bin/env perl
+
+# pstamp_get_calib_info.pl
+
+use warnings;
+use strict;
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+use Carp;
+
+use IPC::Cmd 0.36 qw( can_run run );
+#use File::Temp qw( tempfile );
+#use File::Copy;
+#use File::Basename qw(dirname);
+
+use PS::IPP::Metadata::Config;
+# use PS::IPP::Metadata::Stats;
+use PS::IPP::Metadata::List qw( parse_md_list );
+
+use PS::IPP::Config qw( :standard );
+#use PS::IPP::PStamp::RequestFile qw( :standard );
+#use PS::IPP::PStamp::Job qw( :standard );
+
+my ( $cam_id, $output, $dbname, $verbose, $save_temps);
+
+GetOptions(
+           'cam_id=s'       => \$cam_id,
+           'output=s'       => \$output,
+	   'dbname=s'       => \$dbname,
+	   'verbose'        => \$verbose,
+	   'save-temps'     => \$save_temps,
+) or pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+
+die "usage: --cam_id <cam_id> --output <output file name> [--dbname dbname --verbose]\n"
+    if !$cam_id or !$output;
+
+my $ipprc = PS::IPP::Config->new(); # IPP Configuration
+
+my $missing_tools;
+
+my $releasetool = can_run('releasetool') or (warn "Can't find releasetool" and $missing_tools = 1);
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files
+
+my $relexp;
+{
+    my $command = "$releasetool -listrelexp -state calibrated -priority_order -limit 1 -cam_id $cam_id";
+    $command   .= " -dbname $dbname" if $dbname;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        die("Unable to perform $command error code: $error_code");
+    }
+    my $data = join "", @$stdout_buf;
+    if ($data) {
+        # print STDERR $data if $verbose;
+
+        my $metadata = $mdcParser->parse($data) or die("Unable to parse metdata config doc");
+
+        # no need to use parse_md_fast here
+        my $results = parse_md_list($metadata);
+        if (scalar @$results != 1) {
+            print STDERR "get_job_params: failed to parse_md_list\n";
+            exit 0;
+        }
+
+        $relexp = $results->[0];
+    }
+}
+
+
+my $OUT;
+if ($output eq '-') {
+    $OUT = *STDOUT;
+} else {
+    open $OUT, ">$output" or die "failed to open $output for writing\n";
+}
+
+if (!$relexp) {
+    print STDERR "Failed to find calibration information for camRun: $cam_id\n";
+    print $OUT "DVOCALIB     STR         F # exposure not calibrated with DVO\n";
+    exit 0;
+}
+
+my $ubercal_exp = ($relexp->{flags} & 0x200) ? "T # exposure has ubercal zero point" : "F # exposure does not have ubercal zero point";
+
+print $OUT "DVOCALIB     STR         T # exposure calibrated with DVO\n";
+print $OUT "ZPCALIB      F32         $relexp->{zpt_obs} # calibrated zero point\n";
+print $OUT "ZPCALERR     F32         $relexp->{zpt_stdev} # calibrated zero point error - 0 for ubercal exposure\n";
+print $OUT "MCAL         F32         $relexp->{mcal} # zero point offset due to clouds\n";
+print $OUT "UCALEXP      STR         $ubercal_exp\n";
+print $OUT "DVOFLAGS     U32         $relexp->{flags} # dvo flags\n";
+print $OUT "UCALDIST     S32         $relexp->{ubercal_dist} # distance to ubercal image\n";
+print $OUT "DVODB        STR         $relexp->{dvodb} # dvo database used for calibration \n";
+print $OUT "UCALFILE     STR         $relexp->{ubercal_file} # ubercal file used for calibration\n";
+print $OUT "CALDATE      STR         $relexp->{time_stamp} # time of calibration\n";
+print $OUT "PSREL        STR         $relexp->{release_name} # PS release name\n";
+
+close $OUT or die "failed to close $output\n";
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/psmkreq
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/psmkreq	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/psmkreq	(revision 41103)
@@ -0,0 +1,448 @@
+#!/usr/bin/env perl
+###
+### psmkreq
+###
+###     Program to make a postage stamp request table for a set of coordinates using supplied command
+###     line options and defaults
+###
+
+use warnings;
+use strict;
+
+use Getopt::Long qw( GetOptions ) ; # :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+use PS::IPP::Config qw( :standard );
+use PS::IPP::PStamp::RequestFile qw( :standard );
+use PS::IPP::PStamp::Job qw( :standard );
+use File::Temp qw(tempfile);
+use File::Basename qw(basename);
+use IPC::Cmd 0.36 qw( can_run run );
+use Carp;
+use POSIX;
+
+my $verbose;
+my $save_temps;
+
+# list file columns
+# RA DEC FILTER MJD_MIN MJD_MAX | COMMENT
+
+my ($ra, $dec, $x, $y, $list, $output, $req_name, $req_name_base);
+
+my ($image, $mask, $variance, $jpeg, $allroi, $cmf, $psf, $backmdl, $inverse, $restorebackground);
+my ($exptime, $expnum, $exptimejpeg, $expnumjpeg);
+my ($convolved, $unconvolved, $uncompressed, $use_imfile_id, $no_wait);
+
+# new header keywords for version 2
+my $action = 'PROCESS';
+my $email = 'null';
+
+my $default_size = 100;
+my $job_type     = 'stamp';
+my $req_type     = 'bycoord';
+my $stage        = 'stack';
+my $option_mask;
+my $width        = $default_size;
+my $height       = $default_size;
+my $whole_file   = 0;
+my $project      = 'gpc1';
+my $coord_mask;
+my $pixcenter;
+my $arcseconds;
+
+my $id;
+my $tess_id = 'null';
+my $component = 'null';
+my $data_group = 'null';
+my $filter;
+my $mjd_min = 0;
+my $mjd_max = 0;
+my $comment;
+
+# new request specification columns for version 2
+my $survey_name ='null';
+my $release_name = 'null';
+my $run_type ='null';
+my $fwhm_min = 0;
+my $fwhm_max = 0;
+
+
+my $missing_tools;
+my $pstamp_request_file  = can_run('pstamp_request_file')  or (warn "Can't find required program pstamp_request_file"  and $missing_tools = 1);
+
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+GetOptions(
+    'list=s'            => \$list,          # list of coordinates if undef ra and dec or x and y are required
+    'ra=s'              => \$ra,
+    'dec=s'             => \$dec,
+    'x=s'               => \$x,
+    'y=s'               => \$y,
+    'width=i'           => \$width,
+    'height=i'          => \$height,
+    'whole-file'        => \$whole_file,
+    'pixcenter'         => \$pixcenter,
+    'arcseconds'        => \$arcseconds,
+    'coord_mask=i'      => \$coord_mask,
+    'job_type=s'        => \$job_type,
+    'output=s'          => \$output,
+    'req_name=s'        => \$req_name,
+    'req_name_base=s'   => \$req_name_base,
+    'stage=s'           => \$stage,
+    'project=s'         => \$project,
+    'req_type=s'        => \$req_type,
+    'id=s'              => \$id,
+    'tess_id=s'         => \$tess_id,
+    'component=s'       => \$component,
+    'data_group=s'      => \$data_group,
+    'filter=s'          => \$filter,
+    'mjd_min=s'         => \$mjd_min,
+    'mjd_max=s'         => \$mjd_max,
+    'comment=s'         => \$comment,
+
+    'survey=s'          => \$survey_name,
+    'release=s'         => \$release_name,
+    'run_type=s'        => \$run_type,
+    'fwhm_min=s'        => \$fwhm_min,
+    'fwhm_max=s'        => \$fwhm_max,
+
+    'option_mask=i'     => \$option_mask,
+    'image'             => \$image,
+    'mask'              => \$mask,
+    'jpeg'              => \$jpeg,
+    'allroi'            => \$allroi,
+    'variance'          => \$variance,
+    'cmf'               => \$cmf,
+    'psf'               => \$psf,
+    'backmdl'           => \$backmdl,
+    'exptime'           => \$exptime,
+    'expnum'            => \$expnum,
+    'exptimejpeg'       => \$exptimejpeg,
+    'expnumjpeg'        => \$expnumjpeg,
+    'inverse'           => \$inverse,
+    'unconvolved'       => \$unconvolved,
+    'convolved'         => \$convolved,
+    'uncompressed'      => \$uncompressed,
+    'restorebackground' => \$restorebackground,
+    'use_imfile_id'     => \$use_imfile_id,
+    'do_not_wait'       => \$no_wait,
+
+    'action=s'          => \$action,
+    'email=s'           => \$email,
+
+    'verbose'           => \$verbose,
+    'save-temps'        => \$save_temps,
+) or pod2usage(2);
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+
+pod2usage( -msg => "Required options: --req_name | --req_name_base --stage ") 
+    unless (defined $req_name or defined $req_name_base)
+       and defined $stage;
+
+pod2usage( -msg => "Invalid job_type: $job_type", -exitval =>1 )
+        unless ($job_type eq 'stamp' or $job_type eq 'get_image');
+
+if ($job_type eq 'stamp') {
+    if (defined $list) {
+        pod2usage( -msg => "--ra --dec --x --y are not used with --list", -exitval =>1 )
+            if defined $x or defined $y;
+    } else {
+        if ($whole_file) {
+            $pixcenter = 1;
+            $x = 0;
+            $y = 0;
+            $width = 0;
+            $height = 0;
+        }
+        if (!$pixcenter) {
+            pod2usage( -msg => "Required options for stamp requests: --ra and --dec or --list", -exitval =>1 )
+                unless (defined $ra and defined $dec);
+            # to simplify code we just use $x and $y from here
+            $x = $ra;
+            $y = $dec;
+        } else {
+            pod2usage( -msg => "Required options for stamp requests: --x and --y or --list", -exitval =>1 )
+                    unless (defined $x and defined $y);
+        }
+    }
+} else {
+    pod2usage( -msg => "req_type must be byid or byexp for get_image requests",
+        -exitval =>1 ) unless $req_type eq 'byid' or $req_type eq 'byexp';
+    $x  = 0;
+    $y = 0;
+}
+
+if ($req_type eq 'byid') {
+    pod2usage( -msg => "ID required for get_image requests", -exitval =>1 )
+            if !defined $id;
+    die("ID must be number for byid requests") if !validID($id);
+} elsif ($req_type eq 'byexp') {
+    pod2usage( -msg => "ID (exp_name) required for byexp requests", -exitval =>1 )
+            if !defined $id;
+} elsif ($req_type eq 'bydiff') {
+    pod2usage( -msg => "ID (exp_name) required for bydiff requests", -exitval =>1 )
+            if !defined $id;
+} elsif ($req_type eq 'byskycell') {
+    pod2usage( -msg => "tess_id and skycell_id required for byskycell requests", -exitval =>1 )
+            if !(defined $tess_id and defined $component);
+} elsif ($req_type ne 'bycoord') {
+    pod2usage( -msg => "$req_type is not a valid req_type", -exitval =>1 );
+}
+
+$id = 0 if !$id;
+
+unless ($stage eq 'raw' or $stage eq 'chip' or $stage eq 'warp' or $stage eq 'diff' or $stage eq 'stack'
+    or $stage eq 'stack_summary') {
+    die "$stage is not a valid value for stage\n";
+}
+
+if ($stage eq 'stack') {
+    # default is to do convolved stack
+    unless (defined $option_mask || $convolved) {
+        $unconvolved = 1;
+    }
+}
+
+checkFilter($filter, 'null', $filter)  if $filter;
+checkMJD($mjd_min, 0, "") if $mjd_min;
+checkMJD($mjd_max, 0, "" ) if $mjd_min;
+
+# user supplied option mask takes precedence
+if (!defined $option_mask) {
+    $option_mask = 0;
+    if ($job_type eq 'stamp') {
+        $option_mask |= $PSTAMP_SELECT_IMAGE    if $image;
+        $option_mask |= $PSTAMP_SELECT_MASK     if $mask;
+        $option_mask |= $PSTAMP_SELECT_VARIANCE if $variance;
+        $option_mask |= $PSTAMP_SELECT_JPEG     if $jpeg;
+        $option_mask |= $PSTAMP_MULTI_OVERLAP_IMAGE     if $allroi;
+
+        # if no image was requested make a stamp of the image
+        $option_mask = $PSTAMP_SELECT_IMAGE    if $option_mask == 0;
+
+        $option_mask |= $PSTAMP_SELECT_CMF      if $cmf;
+        $option_mask |= $PSTAMP_SELECT_PSF      if $psf;
+        $option_mask |= $PSTAMP_SELECT_BACKMDL  if $backmdl;
+        $option_mask |= $PSTAMP_SELECT_EXP      if $exptime;
+        $option_mask |= $PSTAMP_SELECT_NUM      if $expnum;
+        $option_mask |= $PSTAMP_SELECT_EXPJPEG  if $exptimejpeg;
+        $option_mask |= $PSTAMP_SELECT_NUMJPEG  if $expnumjpeg;
+        $option_mask |= $PSTAMP_SELECT_UNCOMPRESSED if $uncompressed;
+        $option_mask |= $PSTAMP_SELECT_UNCONV   if $unconvolved;
+        $option_mask |= $PSTAMP_USE_IMFILE_ID   if $use_imfile_id;
+        $option_mask |= $PSTAMP_SELECT_INVERSE  if $inverse;
+        $option_mask |= $PSTAMP_RESTORE_BACKGROUND  if $restorebackground;
+    }
+    $option_mask |= $PSTAMP_NO_WAIT_FOR_UPDATE if $no_wait;
+}
+
+
+# user supplied coord_mask takes precedence
+if (!defined $coord_mask) {
+    $coord_mask = 0;
+    if ($pixcenter) {
+        $coord_mask |= $PSTAMP_CENTER_IN_PIXELS;
+    }
+    if (!$arcseconds) {
+        $coord_mask |= $PSTAMP_RANGE_IN_PIXELS;
+    }
+}
+
+
+# if req_name is not supplied use the current date and time to build one off of the base
+if (!$req_name) {
+    my $datestr = strftime "%Y%m%dT%H%M%S", gmtime;
+    $req_name .= $req_name_base . $datestr;
+}
+
+# ok ready to go
+
+my $rows;
+if ($list) {
+    $rows = readList($list);
+} else {
+    $rows = [];
+    push @$rows, buildRow("", $comment, 1, $x, $y, $filter, $mjd_min, $mjd_max);
+}
+
+my ($tdf, $table_def_name) = tempfile ("/tmp/tabledef.XXXX", UNLINK => !$save_temps);
+my $line = "$req_name 2 $action $email";
+print "$line\n" if $verbose;
+print $tdf "$line\n";
+my $rownum = 0;
+foreach my $row (@$rows) {
+    $rownum++;
+    my $line = "$rownum $row->{ra}\t$row->{dec}\t$width $height"
+        . " $coord_mask $job_type $option_mask $project $survey_name $release_name $req_type"
+        . " $stage $id $tess_id $component $data_group"
+        . " $row->{filter} $row->{mjd_min} $row->{mjd_max} $run_type $fwhm_min $fwhm_max";
+
+    if ($row->{comment} and $row->{comment} ne '') {
+        $line .= " |$row->{comment}" ;
+    } else {
+        $line .= " |$comment" if $comment;
+    }
+
+    print "$line\n" if $verbose;
+    print $tdf "$line\n";
+}
+close $tdf;
+
+{
+    my $command = "$pstamp_request_file --input $table_def_name --req_name $req_name";
+    $command .= " --output $output" if $output;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        print STDERR @$stderr_buf;
+        exit $error_code >> 8;
+    }
+}
+
+exit 0;
+
+sub readList {
+    my $file = shift;
+    open IN, "<$file" or die "failed to open $file for input\n";
+
+    my @rows;
+
+    my $linenumber = 0;
+    foreach my $line (<IN>) {
+        $linenumber++;
+        chomp $line;
+        next if !$line;
+        next if ($line =~ /^#/);
+
+        my ($spec, $comment) = split /\|/, $line;
+        $comment = '' if !defined $comment;
+
+        my @vals = split " ", $spec;
+        push @rows, buildRow("at line number $linenumber", $comment, @vals);
+    }
+    close IN;
+
+    return \@rows;
+}
+
+sub buildRow {
+    my $linenumber = shift;
+    my $comment = shift;
+    my @vals = @_;
+
+    my $row = {};
+    $row->{rownum}  = checkRownum($vals[0], $linenumber);
+    $row->{ra}      = checkRA($vals[1], $linenumber);
+    $row->{dec}     = checkDEC($vals[2], $linenumber);
+    $row->{filter}  = checkFilter($vals[3], $filter, $linenumber);
+    $row->{mjd_min} = checkMJD($vals[4], $mjd_min, $linenumber);;
+    $row->{mjd_max} = checkMJD($vals[5], $mjd_max, $linenumber);;
+    $row->{comment} = $comment;
+
+    return $row;
+}
+
+sub validNumber {
+    my $val = shift;
+
+    return 0 if !defined $val;
+
+    return ($val =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/);
+}
+
+sub validID
+{
+    my $val = shift;
+
+    return 0 if !$val;
+
+    return  ! ($val =~ /\D/);
+}
+
+sub checkCoord {
+    my $c          = shift;
+    my $linenumber = shift;
+
+   my $result;
+    if ($c =~ /\:/) {
+        # sexagesmial format not valid for pixel coordinates
+        die "invalid coordinate value $c found $linenumber\n" if $pixcenter;
+
+        my ($d, $m, $s) = split '\:', $c;
+        die "invalid coordinate value $c found $linenumber\n" 
+            unless validNumber($d) and validNumber($m) and validNumber($s);
+        my $sign;
+        if ($c =~ /^-/) {   # checking $d < 0 doesn't work for $d = -0
+            $sign = -1;
+            $d = -$d;
+        } else {
+            $sign = 1;
+        }
+        $result = $sign * ($d + ($m + $s/60.) /60.);
+    } else {
+        die "$c is not  valid coordinate value $linenumber\n" 
+            unless validNumber($c);
+        $result = $c;
+    }
+    return $result;
+}
+
+sub checkRownum {
+    my $rownum = shift;
+    my $linenumber = shift;
+    die "rownum can not be null at $linenumber\n" unless defined $rownum;
+
+    # XXX: For now just check that rownum is defined and a single word
+    # extend this. Should we require a number?
+    my @words = split " ", $rownum;
+    die "$rownum is not a vaild rownum at $linenumber\n" unless (scalar @words) == 1;
+}
+
+sub checkRA {
+    my $ra = shift;
+    my $linenumber = shift;
+    my $checked = checkCoord($ra, $linenumber);
+
+    if ($ra =~ /\:/) {
+        # assume RA is in hours:minutes:seconds
+        return $checked * 360. / 24.;
+    } else {
+        return $checked;
+    }
+}
+
+sub checkDEC {
+    return checkCoord(@_);
+}
+
+sub checkFilter {
+    my $f = shift;
+    my $default = shift;
+    my $linenumber = shift;
+
+    $default = 'null' if !defined $default;
+
+    $f = $default if (!defined$f or $f eq 'null');
+
+    return $f;
+}
+
+sub checkMJD {
+    my $mjd = shift;
+    my $default = shift;
+    my $linenumber = shift;;
+
+    return $default if (!$mjd) ;
+
+    die "invalid mjd value '$mjd' found $linenumber\n" if !validNumber($mjd);
+
+    return $mjd;
+}
+
+sub checkStage {
+    my $stage = shift;
+
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/psstatus
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/psstatus	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/psstatus	(revision 41103)
@@ -0,0 +1,299 @@
+#!/bin/env perl
+
+use strict;
+use warnings;
+
+use DBI;
+use PS::IPP::Config 1.01 qw( :standard );
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+use File::Temp qw(tempfile);
+
+my $limit = 0;
+my $running;
+my $finished;
+my $finishing;
+my $totals;
+my $dbname;
+my $dbserver;
+my $dbuser;
+my $dbpassword;
+my $verbose;
+
+GetOptions(
+    'running|r',     \$running,
+    'finished|f',    \$finished,
+    'finishing',     \$finishing,
+    'totals|t',      \$totals,
+    'limit|l=i',     \$limit,
+    'dbname=s',      \$dbname,
+    'verbose|v',     \$verbose,
+) or pod2usage (2);
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV ;
+
+$running = 1 if (!$finished and !$finishing and !$running and !$totals);
+
+my $no_args = ! (defined $running or defined $finished or defined $finishing or defined $totals );
+
+die "cannot supply --running and --finished\n" if ($running and $finished);
+    
+
+my $ipprc =  PS::IPP::Config->new();
+my $dbh = getDBHandle();
+
+my $totals_query = "
+SELECT label,
+    count(job_id) AS 'Unfinished Jobs',
+    IFNULL(priority,10000) as priority
+FROM pstampRequest join pstampJob USING(req_id)
+LEFT JOIN Label USING(label)
+WHERE pstampJob.state ='run' and pstampRequest.state ='run'
+GROUP by label";
+
+my $running_query = "
+SELECT 
+    unfinishedReq.req_id,
+    name,
+    label,
+    reqType,
+   -- priority,
+    state,
+    fault,
+    rowcount AS 'Num Rows',
+    jobcount AS 'Total Jobs',
+    IFNULL(completedJobs,0) AS 'Completed Jobs',
+    IFNULL(runningJobs, 0) AS 'Pending Jobs',
+    IFNULL(faultedJobs, 0) AS 'Faulted Jobs',
+    IFNULL(depcount, 0) AS 'Image updates'
+    , timestamp AS 'last state change (UTC)'
+    , registered AS 'Submit time (UTC)'
+FROM (
+    SELECT
+        req_id,
+        name,
+        label,
+        reqType,
+        pstampRequest.state,
+        pstampRequest.fault,
+        timestamp,
+        registered,
+        IFNULL(priority,10000) AS priority
+FROM pstampRequest
+LEFT JOIN Label USING(label)
+    WHERE pstampRequest.state = 'run' or pstampRequest.state ='new' 
+    	OR pstampRequest.state = 'parsed'
+   --     OR pstampRequest.state = 'run.wait'
+) as unfinishedReq
+LEFT JOIN (
+    SELECT req_id,
+        count(job_id)          AS jobcount,
+        count(distinct rownum) AS rowcount
+FROM pstampRequest JOIN pstampJob USING(req_id)
+    WHERE (pstampRequest.state = 'run' or pstampRequest.state = 'new' 
+        OR pstampRequest.state = 'run.wait'
+        OR pstampRequest.state = 'parsed')
+    GROUP BY req_id
+) as rowCounts
+ON unfinishedReq.req_id = rowCounts.req_id
+
+LEFT JOIN (
+    SELECT req_id,
+        count(distinct dep_id) AS depcount
+FROM pstampRequest
+    JOIN pstampJob USING(req_id)
+    JOIN pstampDependent USING(dep_id) 
+    WHERE (pstampRequest.state = 'run' or pstampRequest.state = 'new' 
+	    OR pstampRequest.state = 'parsed'
+            OR pstampRequest.state = 'run.wait')
+        AND dep_id > 0
+        AND pstampDependent.state = 'full'
+    GROUP BY req_id
+) as depCounts
+ON unfinishedReq.req_id = depCounts.req_id
+
+LEFT JOIN (
+    SELECT req_id,
+        count(job_id) AS runningJobs
+    FROM pstampRequest JOIN pstampJob USING(req_id)
+    WHERE (pstampRequest.state = 'run' or pstampRequest.state ='new' 
+        OR pstampRequest.state = 'run.wait')
+        AND pstampJob.state = 'run'
+        AND pstampJob.fault = 0
+    GROUP BY req_id
+) as runningJobs
+ON unfinishedReq.req_id = runningJobs.req_id
+
+LEFT JOIN (
+    SELECT req_id,
+        count(job_id) AS faultedJobs
+    FROM pstampRequest JOIN pstampJob USING(req_id)
+    WHERE (pstampRequest.state = 'run' or pstampRequest.state ='new'
+        OR pstampRequest.state = 'run.wait')
+        -- AND pstampJob.state = 'run'
+        AND pstampJob.fault > 0
+    GROUP BY req_id
+) as faultedJobs
+ON unfinishedReq.req_id = faultedJobs.req_id
+LEFT JOIN (
+    SELECT req_id,
+        count(job_id) AS completedJobs
+    FROM pstampRequest JOIN pstampJob USING(req_id)
+    WHERE (pstampRequest.state = 'run' or pstampRequest.state ='new'
+        OR pstampRequest.state = 'run.wait')
+        AND pstampJob.state = 'stop'
+    GROUP BY req_id
+) as finishedJobs
+ON unfinishedReq.req_id = finishedJobs.req_id
+
+ORDER by unfinishedReq.req_id
+";
+
+my $finishing_query = "
+SELECT
+    req_id,
+    name,
+    label,
+    reqType,
+    pstampRequest.state,
+    pstampRequest.fault,
+    count(job_id) as 'Finished jobs',
+    timestamp as 'last state change'
+FROM pstampRequest
+    JOIN pstampJob USING(req_id)
+WHERE pstampRequest.state ='run'
+    AND (
+        SELECT count(job_id) FROM pstampJob
+            WHERE pstampJob.req_id = pstampRequest.req_id
+                AND pstampJob.state != 'stop'
+    ) = 0
+GROUP BY req_id
+ORDER by req_id
+";
+
+my $finished_query = "
+SELECT 
+    finishedReq.req_id,
+    name,
+    label,
+    reqType,
+    rowcount AS 'Num Rows',
+    jobcount AS 'Total Jobs',
+    IFNULL(success, 0) AS 'Successful Jobs',
+    IFNULL(faulted,0) AS 'Faulted Jobs',
+    IFNULL(depcount, 0) AS 'Image updates completed',
+    timestamp AS 'Completion Time (UTC)',
+    registered AS 'Submit Time (UTC)'
+FROM (
+    SELECT
+    req_id,
+    name,
+    label,
+    reqType,
+--    pstampRequest.state,
+--    pstampRequest.fault,
+    timestamp,
+    registered
+FROM pstampRequest
+    WHERE pstampRequest.state = 'stop'
+        AND date_add(timestamp, interval 1 day) >=utc_timestamp()
+) as finishedReq
+
+LEFT JOIN (
+    SELECT req_id,
+        count(job_id)          AS jobcount,
+        count(distinct rownum) AS rowcount
+FROM pstampRequest JOIN pstampJob USING(req_id)
+    WHERE pstampRequest.state = 'stop'
+        AND date_add(timestamp, interval 1 day) >=utc_timestamp()
+    GROUP BY req_id
+) as rowCounts
+ON finishedReq.req_id = rowCounts.req_id
+
+LEFT JOIN (
+    SELECT req_id,
+        count(distinct dep_id) AS depcount
+FROM pstampRequest JOIN pstampJob USING(req_id)
+    WHERE pstampRequest.state = 'stop'
+        AND date_add(timestamp, interval 1 day) >=utc_timestamp()
+        AND dep_id > 0
+    GROUP BY req_id
+) as depCounts
+ON finishedReq.req_id = depCounts.req_id
+
+LEFT JOIN (
+    SELECT req_id,
+        count(job_id) AS success
+    FROM pstampRequest JOIN pstampJob USING(req_id)
+    WHERE pstampRequest.state = 'stop'
+        AND date_add(timestamp, interval 1 day) >=utc_timestamp()
+        AND pstampJob.fault = 0
+    GROUP BY req_id
+) as successfulJobs
+ON finishedReq.req_id = successfulJobs.req_id
+LEFT JOIN (
+    SELECT req_id,
+        count(job_id) AS faulted
+    FROM pstampRequest JOIN pstampJob USING(req_id)
+    WHERE pstampRequest.state = 'stop'
+        AND date_add(timestamp, interval 1 day) >=utc_timestamp()
+        AND pstampJob.fault > 0
+    GROUP BY req_id
+) as faultedJobs
+ON finishedReq.req_id = faultedJobs.req_id
+ORDER by timestamp desc
+";
+
+
+my $sql;
+if ($totals) {
+    $sql = $totals_query;
+} elsif ($finished) {
+    $sql = $finished_query;
+} elsif ($running) {
+    $sql = $running_query;
+} elsif ($finishing) {
+    $sql = $finishing_query;
+} else {
+    die "can't happen";
+}
+
+if ($limit and !$no_args) {
+    $sql .= " LIMIT $limit";
+}
+
+{
+    print STDERR "$sql\n" if $verbose;
+    my @args = ( "--table", "--user=$dbuser",  "--host=$dbserver", "--password=$dbpassword", $dbname);
+
+    push @args, '--verbose' if $verbose;
+
+    open my $mysql_pipe, "|-", 'mysql', @args or die "failed to run mysql\n";
+    print $mysql_pipe "$sql;\n";
+    close $mysql_pipe;
+
+}
+
+exit 0;
+
+
+sub getDBHandle {
+    $dbserver = metadataLookupStr($ipprc->{_siteConfig}, "PS_DBSERVER");
+    $dbuser = metadataLookupStr($ipprc->{_siteConfig}, "DS_DBUSER");
+    $dbpassword = metadataLookupStr($ipprc->{_siteConfig}, "DS_DBPASSWORD");
+    if (!$dbname) {
+        $dbname = metadataLookupStr($ipprc->{_siteConfig}, "PS_DBNAME");
+    }
+
+    die "database configuration set up" unless defined($dbserver) and defined($dbuser)
+        and defined($dbpassword) and defined($dbname);
+
+    my $dsn = "DBI:mysql:host=$dbserver;database=$dbname";
+
+    my $dbh = DBI->connect($dsn, $dbuser, $dbpassword) 
+        or die "Cannot connect to database.\n";
+
+    return $dbh;
+}
+
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_checkdependent.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_checkdependent.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_checkdependent.pl	(revision 41103)
@@ -0,0 +1,999 @@
+#!/bin/env perl
+#
+# pstamp_checkdependent.pl
+#
+# Check the status of a pending pstampDependent insuring that 
+# the any update processing that is needed has been queued and determine check 
+# whether the processing has  finished or not
+
+use warnings;
+use strict;
+
+use Sys::Hostname;
+use Getopt::Long qw( GetOptions );
+use File::Basename qw(basename);
+use Carp;
+use POSIX;
+use IPC::Cmd 0.36 qw( can_run run );
+use PS::IPP::Metadata::List qw( parse_md_list );
+use PS::IPP::Config qw( :standard );
+use PS::IPP::PStamp::RequestFile qw( :standard );
+use Carp;
+
+my $disable_3PI_updates = 0;
+my $ra_max_3PI_hours = 22.5;  # disable updates for skycells with ra greater than this (hours)
+my $ra_max_3PI = $ra_max_3PI_hours * 15 * 3.14149 / 180.; # rawExp.ra is in radians
+
+# verbose flag for parse commands
+my $parse_verbose = 0;
+
+# XXX: put this in a module somewhere
+my $IPP_DIFF_MODE_WARP_WARP   = 1;
+my $IPP_DIFF_MODE_WARP_STACK  = 2;
+my $IPP_DIFF_MODE_STACK_WARP  = 3;
+my $IPP_DIFF_MODE_STACK_STACK = 4;
+
+my ($dep_id, $stage, $stage_id, $component, $imagedb, $label, $rlabel, $need_magic, $fault_count, $max_fault_count, $logfile);
+my ($dbname, $ps_dbserver, $verbose, $save_temps, $no_update);
+
+GetOptions(
+    'dep_id=i'      =>  \$dep_id,
+    'stage=s'       =>  \$stage,
+    'stage_id=i'    =>  \$stage_id,
+    'component=s'   =>  \$component,
+    'imagedb=s'     =>  \$imagedb,      # dbname for images lookups.
+    'label=s'       =>  \$label,        # request's label
+    'rlabel=s'      =>  \$rlabel,       # pstampDependent.rlabel (deprecated)
+    'need_magic'    =>  \$need_magic,
+    'fault_count=i' =>  \$fault_count,
+    'max_fault_count=i' =>  \$max_fault_count,
+    'logfile=s'	    =>  \$logfile,
+    'dbname=s'      =>  \$dbname,       # postage stamp server dbname
+    'dbserver=s'    =>  \$ps_dbserver,  # postage stamp server dbserver
+    'verbose'       =>  \$verbose,
+    'parse-verbose' =>  \$parse_verbose,
+    'save-temps'    =>  \$save_temps,
+    'no-update'     =>  \$no_update,
+);
+
+die "--dep_id --stage --stage_id --component --imagedb are required"
+    if !(defined $dep_id and defined $stage and defined $stage_id and
+        defined $component and defined $imagedb);
+
+$max_fault_count = 5 if !$max_fault_count;
+$fault_count = 0 if !defined $fault_count;
+
+my $missing_tools;
+my $chiptool = can_run('chiptool') or (warn "Can't find chiptool" and $missing_tools = 1);
+my $warptool = can_run('warptool') or (warn "Can't find warptool" and $missing_tools = 1);
+my $difftool = can_run('difftool') or (warn "Can't find difftool" and $missing_tools = 1);
+my $stacktool = can_run('stacktool') or (warn "Can't find stacktool" and $missing_tools = 1);
+my $magicdstool = can_run('magicdstool') or (warn "Can't find magicdstool" and $missing_tools = 1);
+my $pstamptool = can_run('pstamptool') or (warn "Can't find pstamptool" and $missing_tools = 1);
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+my $ipprc = PS::IPP::Config->new();
+
+if ($logfile) {
+   $ipprc->redirect_output($logfile);
+}
+
+print "\n==== Starting dependency checking for $dep_id $stage $stage_id $component at " . (scalar localtime) . ". ====\n";
+
+if ($label) {
+    # rlabel is deprecated. Use one based on the supplied label parameter which is the current label 
+    # for the request, which may be different than the one given to the dependent when the job was parsed.
+    # XXX: having the convention that update label is 'ps_ud_' . $label of request embedded here 
+    # (and formerly in pstampparse.pl) is not particularly clean but it's simple.
+    my $new_rlabel = 'ps_ud_' . $label;
+    $rlabel = "undefined" if !defined $rlabel;
+    if ($new_rlabel ne $rlabel) {
+        print "Notice: using $new_rlabel instead of $rlabel for update label.\n";
+        $rlabel = $new_rlabel;
+    }
+}
+
+
+if (!$ps_dbserver) {
+    $ps_dbserver =  metadataLookupStr($ipprc->{_siteConfig}, 'PS_DBSERVER');
+}
+$pstamptool  .= " -dbname $dbname" if $dbname;
+$pstamptool  .= " -dbserver $ps_dbserver";
+
+# Append imagedb to the ippTools
+# Note: configured DBSERVER is used for this server
+$chiptool    .= " -dbname $imagedb";
+$warptool    .= " -dbname $imagedb";
+$difftool    .= " -dbname $imagedb";
+$stacktool   .= " -dbname $imagedb";
+$magicdstool .= " -dbname $imagedb";
+
+
+my $tool;
+my $cmd;
+my $dsRun_state = "";
+
+if ($stage eq "chip") {
+    $cmd = "$chiptool -processedimfile -allfiles -chip_id $stage_id -class_id $component";
+} elsif ($stage eq "warp") {
+    $cmd = "$warptool -warped -warp_id $stage_id -skycell_id $component";
+} elsif ($stage eq "diff") {
+    $cmd = "$difftool -diffskyfile -diff_id $stage_id -skycell_id $component";
+} else {
+    my_die("unexpected stage $stage found", $PS_EXIT_PROG_ERROR);
+}
+
+my $it = runToolAndParseExpectOne($cmd, $parse_verbose);
+
+my_die("no components found", $PS_EXIT_PROG_ERROR) if ( !$it);
+
+# Got "it"
+
+my $magic_ok = 0;
+if ($stage eq 'diff') {
+    if ($it->{diff_mode} == $IPP_DIFF_MODE_STACK_STACK) {
+        # stack stack diffs don't need magic, but since the warps need to have the chips destreaked
+        # in order to be processed se set need_magic in the database for all runs
+        # Now the diffs themselves don't need to be destreaked so
+        $magic_ok = 1;
+    }
+}
+
+
+# magic is no longer rquired
+$need_magic = 0;
+$magic_ok =  1;
+my $status = 0;
+if ((($it->{state} eq 'full') or ($it->{state} eq 'update')) and ($it->{data_state} eq 'full')) {
+
+    print "\nDependency Satisfied for $stage $stage_id $component\n";
+    # This Dependency is satisfied. All done. Release the pstampJobs
+    #
+    my $command = "$pstamptool -updatedependent -set_state full -dep_id $dep_id";
+    if (!$no_update) {
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                    run(command => $command, verbose => $verbose);
+        unless ($success) {
+            my_die("failed to set pstampDependent.state to 'full' dep_id: $dep_id",
+                $PS_EXIT_UNKNOWN_ERROR);
+        }
+    } else {
+        print "skipping $command\n";
+    }
+} elsif (($it->{state} eq 'cleaned') or ($it->{state} eq 'goto_cleaned') or ($it->{state} eq 'update')) {
+    #       For warp and diff stages we need to call the 'queue_update' subroutines even if the 
+    #       data_state is update in order to check the state of inputs in earlier stages in the pipeline
+    #       For example if warpSkyfile is in update state but the chipRun that it depends on hasn't
+    #       been updated we need to go and queue the chips for processing.
+
+    my $fault = $it->{fault};
+    if (($fault eq $PSTAMP_GONE) or (($it->{state} eq 'update') and $fault)) {
+        $fault_count++;
+        print "$stage $stage_id $component has fault $fault\n";
+        if ($it->{fault} eq $PSTAMP_GONE) {
+            faultJobs($PSTAMP_GONE);
+            exit 0;
+        } elsif ($fault_count >= $max_fault_count) {
+            print "$stage $stage_id $component has faulted $fault_count times. Giving up\n";
+
+            faultComponent($stage, $stage_id, $component, $PSTAMP_GONE);
+
+            # fault the jobs
+            faultJobs($PSTAMP_GONE);
+            exit 0;
+        }
+
+        # hope the fault is transient.
+        # fault the dependent to give the fault a chance to correct itself
+        my_die("Component faulted on update dep_id: $dep_id", $PS_EXIT_SYS_ERROR);
+    }
+
+    if ($stage eq 'chip') {
+        # check_states_chip takes an array so that check_states_warp can pass it a set of chips
+        my $chips = [$it];
+        $status = check_states_chip($it->{chip_id}, $chips, $rlabel, $need_magic);
+    } elsif ($stage eq 'warp') {
+        $status = check_states_warp($it, $rlabel, $need_magic);
+    } elsif ($stage eq 'diff') {
+        $status = check_states_diff($it, $rlabel, $need_magic);
+    } else {
+        my_die("Unexpected stage found $stage", $PS_EXIT_PROG_ERROR);
+    }
+    if ($status >= $PSTAMP_FIRST_ERROR_CODE) {
+        faultJobs($status);
+    }
+} else {
+    print "${stage}Run $stage_id state is $it->{state} $component data_state is $it->{data_state}\n";
+
+    # detect states that cannot be updated and update the job state
+    # XXX: Perhaps I should be more assertive here and check for the specific states that we know
+    # that we can continue.
+
+    my $state = $it->{state};
+    my $job_fault = 0;
+
+    if ($state eq 'error_cleaned') {
+        $job_fault = $PSTAMP_NOT_AVAILABLE;
+    } elsif (($state =~ /scrub/) or ($state =~ /purge/) or ($state eq 'drop')) {
+        # Component state must have been changed state since dependency was inserted.
+        print STDERR "Dependency cannot be satisfied\n";
+        $job_fault = $PSTAMP_GONE;
+    } elsif ($state =~ /new/) {
+        # Dependency never should have been inserted
+        my_die ("Unexpected state for ${stage}Run $stage_id $state", $PS_EXIT_PROG_ERROR);
+    }
+
+if (0) {
+    if (!$job_fault and ($stage eq 'chip')) {
+        # what about "error_cleaned" ?
+        if (! ($it->{data_state} =~ /cleaned/) ) {
+            # should only get here with data_state 'full' and perhaps destreaking not done
+            my_die ("Unexpected state for ${stage}Run $stage_id $state", $PS_EXIT_PROG_ERROR)
+                if $it->{data_state} ne 'full';
+
+            # chip processing is done, start destreaking.
+            my @chips;
+            push @chips, $it->{class_id};
+            $job_fault = check_states_magicDSRun($stage, $stage_id, \@chips, $rlabel, $need_magic, $it->{raw_magicked}, $it->{magic_ds_id}, $it->{dsRun_state});
+        }
+    }
+}
+
+    if ($job_fault >= $PSTAMP_FIRST_ERROR_CODE) {
+        faultJobs($job_fault);
+    }
+}
+    
+exit $status;
+
+
+sub check_states_chip {
+    my $chip_id = shift;
+    my $metadatas = shift;  # an array of hashes, either from chiptool -processedimfile or warptool -scmap
+    my $rlabel = shift;     # if defined a new label for the chipRun
+    my $need_magic = shift; 
+
+    my $dsRun_state;
+    my $raw_all_magicked = 1; # this gets cleared if any of the inputs aren't destreaked
+    my @chips;
+    my $magic_ds_id;
+
+    my $queued_update = 0;
+    foreach my $chip (@$metadatas) {
+        $dsRun_state = $chip->{dsRun_state};
+        $raw_all_magicked &= ($chip->{raw_magicked} > 0);
+        $magic_ds_id = $chip->{magic_ds_id};
+
+        push @chips, $chip->{class_id};
+
+        if ($disable_3PI_updates && ($chip->{data_group} =~ /ThreePi/)) {
+            # we are getting close to the end of PV2. Do not update PV1 chips if RA is above the limit.
+            my $ra = $chip->{ra};
+            if (!defined($ra) or ($ra > $ra_max_3PI)) {
+                print "3PI updates are currently disabled for RA $ra > $ra_max_3PI (radians)\n";
+                return $PSTAMP_NOT_AVAILABLE;
+            } else {
+                print "allowing 3PI updates RA $ra\n";
+            }
+        } 
+
+        my $state = $chip->{state};
+        my $data_state = $chip->{data_state};
+        my $fault = $chip->{fault};
+        if (($state =~ /error/) or ($state =~ /purged/) or ($state =~ /scrubbed/) or ($state eq 'drop') or
+            ($data_state =~ /error/) or ($data_state =~ /purged/) or ($data_state =~ /scrubbed/) or ($data_state eq 'drop') or
+            ($fault eq $PSTAMP_GONE)) {
+
+            print "chipRun state is $chip->{chip_id} has state: $state class_id $chip->{class_id} data_state: $data_state fault: $fault cannot update.\n";
+            my $error_code;
+            if (($state eq 'error_cleaned') or ($data_state ='error_cleaned')) {
+                $error_code = $PSTAMP_NOT_AVAILABLE;
+            } else {
+                $error_code = $PSTAMP_GONE;
+            }
+            
+            # caller will fault the jobs
+            return $error_code;
+        } elsif ($chip->{state} eq 'goto_cleaned') {
+
+            {
+                print "dependent chip is in state goto_cleaned, changing state to update.\n";
+
+                # cleanup must not be running. Set state to update. If this chip is not 'full' it will be
+                # set to be updated the next time this script is invoked
+                # XXX: In the very unlikely case that this chip run is actually in the running cleanup pantasks
+                # queue things may get confused but we can live with that
+                my $command = "$chiptool -updaterun -set_state update -chip_id $chip_id";
+                $command .= " -set_label $rlabel" if $rlabel;
+
+                if (!$no_update) {
+                    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                                run(command => $command, verbose => $verbose);
+                    unless ($success) {
+                        my_die("failed to change ${stage}Run $stage_id $component from goto_cleaned to update", $PS_EXIT_UNKNOWN_ERROR);
+                    }
+                } else {
+                    print "skipping $command\n";
+                }
+                $queued_update = 1;
+            }
+        } elsif (($chip->{data_state} ne 'update') and ($chip->{data_state} ne 'full')) {
+
+            print "Setting chip imfile $chip_id $chip->{class_id} to be updated.\n";
+
+            # chiptool does more state checking to insure this isn't done prematurely.
+            my $command = "$chiptool -setimfiletoupdate -chip_id $chip_id -class_id $chip->{class_id}";
+            $command .= " -set_label $rlabel" if $rlabel;
+
+            my $update_mode;
+            if ($chip->{data_group} =~ /^LAP.ThreePi.20120706/) {
+                # if this is one of the chipRuns from the PV1 LAP regenerate without using the ppImage configdump file.
+                # XXX: PV2 processing is complete. Don't do this anymore
+                # $update_mode = 1;
+            } elsif ($chip->{data_group} =~ /^M31.rp.2013/) {
+                # Version of M31 processed with recipes and auxiliary masks incompatible with the
+                # current code. Run from scratch which will use compatible versions of the same masks..
+                $update_mode = 1;
+            } 
+            $command .= " -set_update_mode $update_mode" if $update_mode;
+
+            if (!$no_update) {
+                my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                            run(command => $command, verbose => $verbose);
+                unless ($success) {
+                    my_die("failed to queue ${stage}Run $stage_id $component for update", $PS_EXIT_UNKNOWN_ERROR);
+                }
+            } else {
+                print "skipping $command\n";
+            }
+            $queued_update = 1;
+        } elsif ($chip->{state} eq 'cleaned' and $chip->{data_state} eq 'update') {
+            # we've had a number of runs in this limbo state
+
+            print "Changing chip run $chip_id to state update for $chip->{class_id} which is in data_state update.\n";
+            my $command = "$chiptool -updaterun -set_state update -chip_id $chip_id";
+            $command .= " -set_label $rlabel" if $rlabel;
+
+            if (!$no_update) {
+                my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                            run(command => $command, verbose => $verbose);
+                unless ($success) {
+                    my_die("failed to change ${stage}Run $stage_id $component from goto_cleaned to update", $PS_EXIT_UNKNOWN_ERROR);
+                }
+            } else {
+                print "skipping $command\n";
+            }
+            $queued_update = 1;
+        } elsif ($chip->{fault}) {
+            $fault_count++;
+            my $fault =  $chip->{fault};
+
+            if ($fault eq $PSTAMP_GONE) {
+                # caller will fault jobs
+                return $PSTAMP_GONE;
+            } elsif ($fault_count > $max_fault_count) {
+                print "Dependency for $stage $stage_id has been found in fault state $fault_count times. Giving up. Fault: $fault\n";
+                # XXX: stop faulting chips for now
+#                $fault = $PSTAMP_GONE;
+#                faultComponent('chip', $chip->{chip_id}, $chip->{class_id}, $PSTAMP_GONE);
+                $fault = $PSTAMP_NOT_AVAILABLE;
+                return $fault;
+            }
+            # fault the dependent
+            my_die("chip $chip->{chip_id} $chip->{class_id} faulted: $chip->{fault}", $chip->{fault});
+        }
+    }
+
+    return 0;
+}
+
+sub check_states_warp {
+    my $metadata = shift;
+    my $rlabel = shift;     # if defined a new label for the chipRun
+    my $need_magic = shift; 
+    
+    my $exit_status = 0;
+
+    my $raw_all_magicked = 1; # this gets cleared if any of the inputs aren't destreaked
+
+    my $warp_id = $metadata->{warp_id};
+    my $skycell_id = $metadata->{skycell_id};
+    my $state = $metadata->{state};
+    my $data_state = $metadata->{data_state};
+
+    # we are getting close to the end of PV2. optionally do not update 3Pi data.
+    if ($disable_3PI_updates && ($metadata->{data_group} =~ /ThreePi/)) {
+        my $ra = $metadata->{ra};
+        if (!defined $ra or ($ra > $ra_max_3PI)) {
+            print "3PI updates are currently disabled for RA $ra > $ra_max_3PI (radians)\n";
+            return $PSTAMP_NOT_AVAILABLE;
+        } else {
+            print "allowing 3PI updates RA $ra\n";
+        }
+    } 
+
+    if (($state =~ /error/) or ($state =~ /purged/) or ($state =~ /scrubbed/) or ($state eq 'drop') or
+         ($data_state =~ /error/) or ($data_state =~ /purged/) or ($data_state =~ /scrubbed/) or ($data_state eq 'drop')) {
+        print STDERR "warpRun $warp_id $skycell_id has state $state $data_state faulting jobs.\n";
+        my $error_code;
+        if (($state eq 'error_cleaned') or ($data_state eq 'error_cleaned')) {
+            $error_code = $PSTAMP_NOT_AVAILABLE;
+        } else {
+            $error_code = $PSTAMP_GONE;
+        }
+        return $error_code
+    }
+    if ((($data_state eq 'full') or ($data_state eq 'update')) and ($metadata->{fault})) {
+        # fault dependent.
+        my $fault = $metadata->{fault};
+        print STDERR "warp $warp_id $skycell_id faulted with code: $fault.";
+        # XXX: TODO maybe: If ($fault != 2 and $fault != $PSTAMP_GONE) revert the skycell. The warp task only reverts fault 2
+        # and sometimes warps will fault due to nfs problems that end up with fault = 4;.
+        return $fault;
+    }
+    if ($metadata->{quality} ne 0) {
+        print STDERR "warp $warp_id $skycell_id bad quality on update: $metadata->{quality}.\n";
+        return $PSTAMP_GONE;
+    }
+
+    my $skycell = $metadata;
+
+    # if ($skycell->{state} eq 'goto_cleaned' and $skycell->{data_state} eq 'full') {
+    if ($skycell->{state} eq 'goto_cleaned') {
+        # cleanup has been queued, but hasn't finished, probably due to an error or cleanup is not running.
+        # It's "safe" to set the state to update. If the skycell is not full it will be set to update
+        # the next time this task runs
+        print "Change state of warp $warp_id to update.\n";
+        my $command = "$warptool -updaterun -set_state update -warp_id $warp_id";
+        $command .= " -set_label $rlabel" if $rlabel;
+
+        if (!$no_update) {
+            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                        run(command => $command, verbose => $verbose);
+            unless ($success) {
+                my_die("failed to change state of ${stage}Run $stage_id from goto_cleaned to update", $PS_EXIT_UNKNOWN_ERROR);
+            }
+        } else {
+            print "skipping $command\n";
+        }
+        return 0;
+    }
+
+    # get the list of input chips for this skycell
+    my $command = "$warptool -scmap -warp_id $warp_id -skycell_id $skycell_id";
+    my $data = runToolAndParse($command, $parse_verbose);
+    if (!$data or scalar @$data == 0) {
+        # This happens if the chipProcessedImfile disappears which happened when earlier
+        # versions of chiptool -revertprocessedimfile didn't check the chipRun.state before
+        # deleting the row. 
+        print STDERR "failed to find warpSkyCellMap for warpRun $warp_id skycell_id $skycell_id";
+        return $PSTAMP_GONE;
+    }
+
+    my $chips_ready = 1;
+    my @chipsToUpdate;
+    my $chip_id;
+    foreach my $chip (@$data) {
+        # XXX: change tools to include cam_state (camRun.state). If it is not full the warp updates will never run.
+        my $cam_fault = $chip->{cam_fault};
+        if ($chip->{camState} eq 'cleaned') {
+            print STDERR "camRun for $warp_id skycell_id $skycell_id is cleaned so warp cannot be updated.\n";
+            return $PSTAMP_GONE;
+        }
+        if (defined $cam_fault and $cam_fault > 0) {
+            print STDERR "camRun for $warp_id skycell_id $skycell_id is faulted: $cam_fault\n";
+            return $PSTAMP_GONE;
+        }
+        $chip_id = $chip->{chip_id};
+        # XXX: -scmap doesn't have ra and dec. Copy it from the warp
+        $chip->{ra} = $metadata->{ra};
+        $chip->{decl} = $metadata->{decl};
+
+if (0) {
+        # if chip has been magicked before require it to be magicked again
+        # because the warp pending query requires it.
+        if ($chip->{magicked} < 0) {
+            print "Input has been destreaked so we must destreak before warping\n";
+            $need_magic = 1 
+        }
+}
+
+        if ($need_magic and ($chip->{magicked} eq 0)) {
+            my_die("Client requires magic, but chip never magicked. How did this dependent get queued?", $PS_EXIT_PROG_ERROR);
+        }
+
+        if (($chip->{data_state} ne 'full') or ($need_magic and ($chip->{magicked} < 0))) {
+            $chips_ready = 0;
+            $chip->{fault} = $chip->{chip_fault};
+            $chip->{data_group} = $chip->{chip_data_group};
+            push @chipsToUpdate, $chip;
+        } else {
+            # this chip is done
+        }
+    }
+
+    if ($chips_ready and $skycell->{data_state} ne 'update') {
+        # the reason we defer setting the warp to update is so that we can handle error conditions at previous
+        # stages more easily.
+        print "Chips are ready for warp $warp_id $skycell->{skycell_id}. Setting skycell to be updated.\n";
+        my $command = "$warptool -setskyfiletoupdate -warp_id $warp_id -skycell_id $skycell->{skycell_id}";
+        $command .= " -set_label $rlabel" if $rlabel;
+
+        if (!$no_update) {
+            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                        run(command => $command, verbose => $verbose);
+            unless ($success) {
+                my_die("failed to queue ${stage}Run $stage_id $component for update", $PS_EXIT_UNKNOWN_ERROR);
+            }
+        } else {
+            print "skipping $command\n";
+        }
+    } elsif ($chips_ready and $skycell->{data_state} eq 'update' and $skycell->{state} ne 'update') {
+        print "Chips are ready for warp $warp_id $skycell->{skycell_id} but run state is $skycell->{state}. Changing state to update.\n";
+        my $command = "$warptool -updaterun -warp_id $warp_id -set_state update";
+        $command .= " -set_label $rlabel" if $rlabel;
+
+        if (!$no_update) {
+            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                        run(command => $command, verbose => $verbose);
+            unless ($success) {
+                my_die("failed to change state of ${stage}Run $stage_id to update", $PS_EXIT_UNKNOWN_ERROR);
+            }
+        } else {
+            print "skipping $command\n";
+        }
+
+    } elsif (scalar @chipsToUpdate > 0) {
+        my $fault = check_states_chip($chip_id, \@chipsToUpdate, $rlabel, $need_magic);
+        if ($fault) {
+            if ($fault eq $PSTAMP_GONE) {
+                # chip or dsfile that this skycell depends on has faulted in a way that is not recoverable
+                # fault the skycell
+                faultComponent('warp', $warp_id, $skycell->{skycell_id}, $PSTAMP_GONE);
+            }
+            $exit_status = $fault;
+        }
+    }
+
+    # return value may be used as the return status of script so we use zero as success
+    return $exit_status;
+}
+
+sub check_states_diff {
+    my $metadata = shift;
+    my $rlabel = shift;     # if defined a new label for the chipRun
+    my $need_magic = 0; 
+
+    my $diff_id   = $metadata->{diff_id};
+    my $diff_mode = $metadata->{diff_mode};
+    my $skycell = $metadata;
+    my $skycell_id = $skycell->{skycell_id};
+
+    if ($metadata->{state} eq 'goto_cleaned') {
+        # cleanup must not be running. Set state to update. If the skycell is not 'full' it will be
+        # set to be updated the next time this script is invoked.
+        print "Changing state of diffRun $diff_id from goto_cleaned to update.\n";
+        my $command = "$difftool -updaterun -set_state update -diff_id $diff_id";
+        $command .= " -set_label $rlabel" if $rlabel;
+
+        if (!$no_update) {
+            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                        run(command => $command, verbose => $verbose);
+            unless ($success) {
+                my_die("failed to queue ${stage}Run $stage_id $component for update", $PS_EXIT_UNKNOWN_ERROR);
+            }
+        } else {
+            print "skipping $command\n";
+        }
+        return 0;
+    }
+
+    if ($diff_mode == $IPP_DIFF_MODE_WARP_STACK ) {
+        # check the state of the template stack
+        my $command = "$stacktool -sumskyfile -stack_id $skycell->{stack2}";
+        my $stack = runToolAndParseExpectOne($command, $parse_verbose);
+        my_die("failed to find stackSumSkyfile for stack_id $skycell->{stack2}", $PS_EXIT_UNKNOWN_ERROR) if !$stack;
+
+        if ($stack->{state} ne 'full') {
+            print STDERR "template stack for diffRun $diff_id $skycell_id is not in full state faulting jobs\n";
+            faultComponent('diff', $diff_id, $skycell_id, $PSTAMP_GONE);
+            return $PSTAMP_GONE;
+        }
+        print " Stack $skycell->{stack2} is ready.\n";
+
+        # now check the warp
+        $command = "$warptool -warped -warp_id $skycell->{warp1} -skycell_id $skycell_id";
+        my $warp = runToolAndParseExpectOne($command, $parse_verbose);
+        my_die("failed to find warpSkyfile for warpRun $skycell->{warp1} skycell_id $skycell_id", $PS_EXIT_UNKNOWN_ERROR) if !$warp;
+
+        if ($warp->{data_state} ne 'full') {
+            my $warp_status = check_states_warp($warp, $rlabel, $need_magic);
+            if ($warp_status eq $PSTAMP_GONE) {
+                faultComponent('diff', $diff_id, $skycell_id, $PSTAMP_GONE);
+            }
+            return $warp_status;
+        } elsif ($warp->{quality} != 0) {
+            print STDERR "input warp has quality error\n";
+            faultComponent('diff', $diff_id, $skycell_id, $PSTAMP_GONE);
+            return $PSTAMP_GONE;
+        }
+        # warps are ready fall through and queue the diff update
+        print " Warp $skycell->{warp1} $skycell_id is ready.\n";
+    } elsif ($diff_mode eq $IPP_DIFF_MODE_WARP_WARP) {
+        my $command = "$warptool -warped -warp_id $skycell->{warp1} -skycell_id $skycell_id";
+        my $warp1 = runToolAndParseExpectOne($command, $parse_verbose);
+        my_die("failed to find warpSkyfile for warpRun $skycell->{warp1} skycell_id $skycell_id", $PS_EXIT_UNKNOWN_ERROR) if !$warp1;
+
+        my $warps_ready = 1;
+        my $warp_status = 0;
+        if ($warp1->{data_state} ne 'full') {
+            $warps_ready = 0;
+            $warp_status = check_states_warp($warp1, $rlabel, $need_magic);
+            if ($warp_status) {
+                if ($warp_status eq $PSTAMP_GONE) {
+                    faultComponent('diff', $diff_id, $skycell_id, $PSTAMP_GONE);
+                }
+                return $warp_status;
+            }
+        } elsif ($warp1->{quality} != 0) {
+	    print STDERR "warp $warp1->{warp_id} $skycell_id has poor quality: $warp1->{quality}\n";
+	    faultComponent('diff', $diff_id, $skycell_id, $PSTAMP_GONE);
+	    return $PSTAMP_GONE;
+	}
+        $command = "$warptool -warped -warp_id $skycell->{warp2} -skycell_id $skycell_id";
+        my $warp2 = runToolAndParseExpectOne($command, $parse_verbose);
+        my_die("failed to find warpSkyfile for warpRun $skycell->{warp2} skycell_id $skycell_id", $PS_EXIT_UNKNOWN_ERROR) if !$warp2;
+
+        if ($warp2->{data_state} ne 'full') {
+            $warps_ready = 0;
+            $warp_status = check_states_warp($warp2, $rlabel, $need_magic);
+            if ($warp_status eq $PSTAMP_GONE) {
+                faultComponent('diff', $diff_id, $skycell_id, $PSTAMP_GONE);
+            }
+        } elsif ($warp2->{quality} != 0) {
+	    print STDERR "warp $warp2->{warp_id} $skycell_id has poor quality: $warp2->{quality}\n";
+	    faultComponent('diff', $diff_id, $skycell_id, $PSTAMP_GONE);
+	    return $PSTAMP_GONE;
+	}
+
+        if (!$warps_ready) {
+            # don't queue the diff update yet
+            return $warp_status;
+        }
+        # inputs are ready fall through and queue the diff update
+
+    } elsif ($diff_mode == $IPP_DIFF_MODE_STACK_STACK ) {
+        # check the state of the input stack
+        my $command = "$stacktool -sumskyfile -stack_id $skycell->{stack1}";
+        my $stack1 = runToolAndParseExpectOne($command, $parse_verbose);
+        my_die("failed to find stackSumSkyfile for stack_id $skycell->{stack1}", $PS_EXIT_UNKNOWN_ERROR) if !$stack1;
+
+        if ($stack1->{state} ne 'full') {
+            print STDERR "input stack $skycell->{stack1} for diffRun $diff_id $skycell_id is not in full state faulting jobs\n";
+            faultComponent('diff', $diff_id, $skycell_id, $PSTAMP_GONE);
+            return $PSTAMP_GONE;
+        }
+        # check the state of the template stack
+        $command = "$stacktool -sumskyfile -stack_id $skycell->{stack2}";
+        my $stack2 = runToolAndParseExpectOne($command, $parse_verbose);
+        my_die("failed to find stackSumSkyfile for stack_id $skycell->{stack2}", $PS_EXIT_UNKNOWN_ERROR) if !$stack2;
+
+        if ($stack2->{state} ne 'full') {
+            print STDERR "template stack $skycell->{stack2} for diffRun $diff_id $skycell_id is not in full state faulting jobs\n";
+            faultComponent('diff', $diff_id, $skycell_id, $PSTAMP_GONE);
+            return $PSTAMP_GONE;
+        }
+
+        # inputs are ready fall through and queue the diff update
+    } elsif ($diff_mode == $IPP_DIFF_MODE_STACK_WARP ) {
+        # check the state of the input stack
+        my $command = "$stacktool -sumskyfile -stack_id $skycell->{stack1}";
+        my $stack = runToolAndParseExpectOne($command, $parse_verbose);
+        my_die("failed to find stackSumSkyfile for stack_id $skycell->{stack1}", $PS_EXIT_UNKNOWN_ERROR) if !$stack;
+
+        if ($stack->{state} ne 'full') {
+            print STDERR "input stack for diffRun $diff_id $skycell_id is not in full state faulting jobs\n";
+            faultComponent('diff', $diff_id, $skycell_id, $PSTAMP_GONE);
+            return $PSTAMP_GONE;
+        }
+
+        # now check the template warp
+        $command = "$warptool -warped -warp_id $skycell->{warp2} -skycell_id $skycell_id";
+        my $warp = runToolAndParseExpectOne($command, $parse_verbose);
+        my_die("failed to find warpSkyfile for warpRun $skycell->{warp2} skycell_id $skycell_id", $PS_EXIT_UNKNOWN_ERROR) if !$warp;
+
+        if ($warp->{data_state} ne 'full') {
+            my $warp_status = check_states_warp($warp, $rlabel, $need_magic);
+            if ($warp_status eq $PSTAMP_GONE) {
+                faultComponent('diff', $diff_id, $skycell_id, $PSTAMP_GONE);
+            }
+            return $warp_status;
+        }
+        # warps are ready fall through and queue the diff update
+    } else {
+        my_die("unexpected diff_mode found: $diff_mode", $PS_EXIT_PROG_ERROR);
+    }
+
+    if (($skycell->{data_state} ne 'update') or ($skycell->{state} ne 'update')) {
+        my $command = "$difftool -setskyfiletoupdate -diff_id $diff_id -skycell_id $skycell_id";
+        $command .= " -set_label $rlabel" if $rlabel;
+
+        if (!$no_update) {
+            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                        run(command => $command, verbose => $verbose);
+            unless ($success) {
+                my_die("failed to queue ${stage}Run $stage_id $component for update", $PS_EXIT_UNKNOWN_ERROR);
+            }
+        } else {
+            print "skipping $command\n";
+        }
+    } elsif (defined $rlabel and ($skycell->{label} ne $rlabel)) {
+        # change the label to match this dependent's rlabel
+        my $command = "$difftool -updatrun -diff_id $diff_id -set_label $rlabel";
+
+        if (!$no_update) {
+            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                        run(command => $command, verbose => $verbose);
+            unless ($success) {
+                my_die("failed to queue ${stage}Run $stage_id $component for update", $PS_EXIT_UNKNOWN_ERROR);
+            }
+        } else {
+            print "skipping $command\n";
+        }
+    } else {
+        print " diff is in update state\n";
+    }
+
+    # return value is a unix style exit status so zero is good
+    return 0;
+}
+
+
+# run a command that produces metadata output and parse the results into an array of objects
+sub runToolAndParse {
+    my $command = shift;
+    my $verbose = shift;
+
+    my ($program) = split " ", $command;
+    $program = basename($program);
+
+    print "Running $command\n" if !$verbose;
+    my $start_tool = DateTime->now->mjd;
+    # run the command and parse the output
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                run(command => $command, verbose => $verbose);
+    unless ($success) {
+        print STDERR @$stderr_buf if !$verbose;
+        return undef;
+    }
+
+    my $now = DateTime->now->mjd;
+    my $dtime_tool = (DateTime->now->mjd - $start_tool) * 86400.;
+    if ($dtime_tool > 0.1) {
+        print "Time to run $program: $dtime_tool\n";
+    }
+
+    my $buf = join "", @$stdout_buf;
+    if (!$buf) {
+        return undef;
+    }
+
+    my $start_parse = DateTime->now->mjd;
+
+    my $mdcParser = PS::IPP::Metadata::Config->new;
+    my $results = parse_md_fast($mdcParser, $buf)
+        or my_die ("Unable to parse metadata config doc", $PS_EXIT_UNKNOWN_ERROR);
+
+    my $dtime_parse = (DateTime->now->mjd - $start_parse) * 86400.;
+    if ($dtime_parse > 0.1) {
+        print "Time to parse results from $program: $dtime_parse\n";
+    }
+
+    return $results;
+}
+
+# run an command returning metadata where we expect 1 entry
+sub runToolAndParseExpectOne {
+    my $command = shift;
+    my $verbose = shift;
+
+    my $data = runToolAndParse($command, $verbose);
+
+    if (!$data) {
+        return undef;
+    }
+
+    my $n = scalar @$data;
+    if ($n > 1) {
+        my_die("Unexpected number of components $n returned by $command", $PS_EXIT_PROG_ERROR);
+    }
+
+    return $data->[0];
+}
+
+# splits meta data config input stream into single units to work around the pathalogically
+# slow parser. This is similar to and adapted from code in various ippScripts.
+sub parse_md_fast {
+    my $mdcParser = shift;
+    my $input = shift;
+    my $output = ();
+
+    my @whole = split /\n/, $input;
+    my @single = ();
+
+    my $n;
+    while ( ($n = @whole) > 0) {
+        my $value = shift @whole;
+        push @single, $value;
+        if ($value =~ /^\s*END\s*$/) {
+	    push @single, "\n";
+
+            my $list = parse_md_list( $mdcParser->parse( join("\n", @single ) ) ) or
+                print STDERR "Unable to parse metdata config doc" and return undef;
+            push @$output, $list->[0];
+
+            @single = ();
+        }
+    }
+    return $output;
+}
+
+# Check the data_state of the magicDSFile associated with this component (currently only chip stage is supported)
+# Returns zero on success.
+# returns the PSTAMP fault code to use for the jobs if an unrecoverable error is detected
+# And faults the dependent if transient errors occur
+sub check_states_magicDSRun {
+    my $stage = shift;
+    my $stage_id = shift;
+    my $components = shift;
+    my $rlabel  = shift;
+    my $need_magic = shift;
+    my $input_magicked = shift;
+    my $magic_ds_id = shift;
+    my $dsRun_state = shift;
+
+    # XXX: this code assumes that for update destreaking is only performed for chip stage
+    my_die ("check_states_magicDSRun only implemented for chip stage", $PS_EXIT_PROG_ERROR) if $stage ne 'chip';
+
+    # if called from check_states_warp dsRun_state is unknown. Go find it.
+    if (!$dsRun_state) {
+        my $command = "$chiptool -listrun -chip_id $stage_id";
+        my $data = runToolAndParse($command, $parse_verbose);
+        my $chipRun = $data->[0];
+        $dsRun_state = $chipRun->{dsRun_state};
+    }
+
+    # if the input file is already magicked no need to queue destreaking for this chipRun
+    if ($need_magic and !$input_magicked) {
+        if (!defined($dsRun_state) or ($dsRun_state eq 'NULL')) {
+            # it is arguably a programming error if we get here
+            print "No magicDSRun for chipRun $stage_id and magic is required\n";
+            return $PSTAMP_NOT_DESTREAKED;
+        } elsif (($dsRun_state eq 'cleaned') or ($dsRun_state eq 'update')) {
+            foreach my $c (@$components) {
+                my $command = "$magicdstool -destreakedfile -magic_ds_id $magic_ds_id -component $c";
+                my $dsfile = runToolAndParseExpectOne($command, $parse_verbose);
+                if (!$dsfile) {
+                    my_die("failed to find magicDSFile for ${stage}Run $stage_id $c", $PS_EXIT_UNKNOWN_ERROR);
+                }
+                if ($dsfile->{fault} eq $PSTAMP_GONE) {
+                    print "magicDSFile has fault $PSTAMP_GONE\n";
+                    return $PSTAMP_GONE;
+                }
+                #  destreak faults get cleared when the component is set to be updated
+                if (($dsfile->{data_state} eq 'update') and ($dsfile->{fault} > 0)) {
+                    $fault_count++;
+                    if ($fault_count > $max_fault_count) {
+                        print "Destreak file $magic_ds_id $component for $stage $stage_id has faulted $fault_count times. Giving up\n";
+                        faultComponent('destreak', $magic_ds_id, $component, $PSTAMP_GONE);
+                        return $PSTAMP_GONE;
+                    }
+                    # Assume fault is transient
+                    my_die("faulted magicDSFile for ${stage}Run $stage_id $c fault: $dsfile->{fault}",
+                        $PS_EXIT_UNKNOWN_ERROR);
+                }
+                if ($dsfile->{data_state} eq 'cleaned') {
+                    $command = "$magicdstool -setfiletoupdate -magic_ds_id $magic_ds_id -component $c";
+		    # XXX: get the recoveryroot from a config file
+                    # (It isn't actually used except to check whether it is a nebulous path)
+                    $command .= " -set_recoveryroot neb://any/gpc1/destreak/recover";
+                    $command .= " -set_label $rlabel" if $rlabel;
+                    if (!$no_update) {
+                        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                            run(command => $command, verbose => $verbose);
+                        unless ($success) {
+                            my_die("failed to set destreaked component to 'update' for ${stage}Run $stage_id $c",
+                                $PS_EXIT_UNKNOWN_ERROR);
+                        }
+                    } else {
+                        print "skipping $command\n";
+                    }
+                } else {
+                    print "magicDSFile for ${stage}Run $stage_id $c data_state: $dsfile->{data_state}\n";
+                }
+            }
+        } elsif ($dsRun_state eq 'failed_revert') {
+            # XXX: revert failures are rarely fixed. give up but say it's just not available not GONE 
+            print "magicDSRun.state = $dsRun_state for chipRun $stage_id is in state failed_revert cannot update\n";
+            return $PSTAMP_NOT_AVAILABLE;
+        } else {
+            print "magicDSRun.state = $dsRun_state for chipRun $stage_id";
+            print " cannot update yet" if $dsRun_state ne "new";
+            print ".\n";
+        }
+    }
+
+    return 0;
+}
+
+sub faultJobs {
+    my ($job_fault) = @_;
+
+    my $command = "$pstamptool -updatejob -set_state stop -set_fault $job_fault -dep_id $dep_id";
+    if (!$no_update) {
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                    run(command => $command, verbose => $verbose);
+        unless ($success) {
+            my_die("failed to set pstampJob.fault for dep_id: $dep_id",
+                $PS_EXIT_UNKNOWN_ERROR);
+        }
+    } else {
+        print "skipping $command\n";
+    }
+}
+
+sub faultComponent {
+    my ($stage, $stage_id, $component, $fault) = @_;
+
+    # Stop faulting components. With the death of magic the likelyhood that something is truly gone
+    # forever is much smaller. The false alarm rate is far too high
+    return;
+
+    my $command;
+    if ($stage eq 'chip') {
+        $command = "$chiptool -updateprocessedimfile -chip_id $stage_id -class_id $component";
+    } elsif ($stage eq 'warp') {
+        $command = "$warptool -updateskyfile -warp_id $stage_id -skycell_id $component";
+    } elsif ($stage eq 'diff') {
+        $command = "$difftool -updatediffskyfile -diff_id $stage_id -skycell_id $component";
+    } elsif ($stage eq 'destreak') {
+        $command = "$magicdstool -updatedestreakedfile -magic_ds_id $stage_id -component $component";
+    } else {
+        my_die("unexpected stage $stage found", $PS_EXIT_PROG_ERROR);
+    }
+
+    $command .= " -fault $fault";
+
+    if (!$no_update) {
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                    run(command => $command, verbose => $verbose);
+        unless ($success) {
+            carp "$cmd failed";
+        }
+    } else {
+        print "skipping $command\n";
+    }
+}
+
+sub my_die
+{
+    my $msg = shift;
+    my $fault = shift;
+    carp $msg;
+
+    my $command = "$pstamptool -updatedependent -set_fault $fault -dep_id $dep_id";
+    if (!$no_update) {
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                    run(command => $command, verbose => $verbose);
+        unless ($success) {
+            carp "$cmd failed";
+        }
+    } else {
+        print "skipping $command\n";
+    }
+
+    exit $fault;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_cleanup.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_cleanup.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_cleanup.pl	(revision 41103)
@@ -0,0 +1,229 @@
+#!/bin/env perl
+###
+### pstamp_cleanup.pl
+### delete all data associated with a postage stamp request
+###  
+### This script should probably be called request_cleanup.pl since it handles more than postage stamp requests
+###
+
+use warnings;
+use strict;
+
+use Sys::Hostname;
+use Getopt::Long qw( GetOptions );
+use File::Basename qw( basename dirname);
+use File::Copy;
+use POSIX qw( strftime );
+use Carp;
+use IPC::Cmd 0.36 qw( can_run run );
+
+use PS::IPP::Metadata::Config;
+use PS::IPP::Metadata::Stats;
+use PS::IPP::Metadata::List qw( parse_md_list );
+
+use PS::IPP::Config qw( :standard );
+use PS::IPP::PStamp::Job qw( :standard );
+
+my $req_id;
+my $uri;
+my $name;
+my $outdir;
+my $reqType;
+my $redirect_output;
+my $product;
+my $label;
+my $verbose;
+my $dbname;
+my $dbserver;
+my $no_update;
+
+GetOptions(
+    'req_id=s'          =>  \$req_id,
+    'name=s'            =>  \$name,
+    'outdir=s'          =>  \$outdir,
+    'reqType=s'         =>  \$reqType,
+    'product=s'         =>  \$product,
+    'uri=s'             =>  \$uri,
+    'redirect-output'   =>  \$redirect_output,
+    'verbose'           =>  \$verbose,
+    'dbname=s'          =>  \$dbname,
+    'dbserver=s'        =>  \$dbserver,
+    'no-update'         =>  \$no_update,
+);
+
+if ($verbose) {
+    my $host = hostname();
+    print "\n\n";
+    print "Starting script $0 on $host\n\n";
+}
+
+my $missing_tools;
+
+my $pstamptool  = can_run('pstamptool')  or (warn "Can't find pstamptool"  and $missing_tools = 1);
+my $dsreg = can_run('dsreg') or (warn "Can't find dsreg" and $missing_tools = 1);
+
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+
+die("--req_id --name --product and --outdir are required") if !defined($req_id) or !defined($product) or !defined($name) or !defined $outdir;
+
+my $ipprc = PS::IPP::Config->new(); # IPP Configuration
+
+my $pstamp_workdir = metadataLookupStr($ipprc->{_siteConfig}, 'PSTAMP_WORKDIR');
+exit ($PS_EXIT_CONFIG_ERROR) unless defined $pstamp_workdir; # lookup failure outputs a message
+
+if (!$dbserver) {
+    $dbserver =  metadataLookupStr($ipprc->{_siteConfig}, 'PS_DBSERVER');
+}
+
+if (0) {
+my_die("Cleanup not yet supported for reqType: $reqType", $req_id, $PS_EXIT_UNKNOWN_ERROR)
+    if ($reqType ne "pstamp") and ($reqType ne "NULL") and ($reqType ne "dquery");
+}
+
+my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files
+
+if ($product ne 'NULL' and $name ne 'NULL') {
+    my $command = "$dsreg --product $product --del $name --rm --force";
+
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        my_die("Unable to perform $command error code: $error_code", $req_id, $PS_EXIT_UNKNOWN_ERROR);
+    }
+}
+
+{
+    my $command = "$pstamptool -listfile -req_id $req_id";
+    $command   .= " -dbname $dbname" if $dbname;
+    $command   .= " -dbserver $dbserver" if $dbserver;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        die("Unable to perform $command error code: $error_code");
+    }
+    my $output = join "", @$stdout_buf;
+    if ($output) {
+        my $files = parse_md_fast($mdcParser, $output);
+        foreach my $file (@$files) {
+            $ipprc->file_delete($file->{path}); 
+        }
+    }
+}
+
+# now go find the workdir for this request 
+# XXX: we finally *have* to store this in the database
+$outdir = undef if $outdir eq "NULL";
+
+if ($outdir) {
+    delete_workdir($outdir);
+} else {
+    if ($uri ne 'NULL') {
+        # use the URI to find the workdir
+        $outdir = dirname($uri);
+        if (index($outdir, $pstamp_workdir) == 0) {
+            print "pstamp workdir for req_id $req_id is $outdir\n";
+            delete_workdir($outdir);
+        } else {
+            print "no outdir found for $req_id\n";
+        }
+    }
+    if (!$outdir) {
+        # gotta go look for it in the subdirectories of $pstamp_workdir
+        my $command = "find $pstamp_workdir -maxdepth 2 -type d -name $req_id";
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+        unless ($success) {
+            my $rc = $error_code >> 8;
+            my_die("Unable to perform $command error code: $error_code", $req_id, $rc);
+        }
+        if ($stdout_buf) {
+            # for historical reasons (a bug) there may be more than one outdir
+            my @outdirs = split "\n", join "", $stdout_buf;
+            foreach $outdir (@outdirs) {
+                chomp $outdir;
+                delete_workdir($outdir);
+            }
+        } else {
+            print "no outdir found for $req_id\n";
+        }
+    }
+}
+
+
+{
+    my $command = "$pstamptool -updatereq -req_id $req_id -set_state cleaned";
+    $command   .= " -dbname $dbname" if $dbname;
+    $command   .= " -dbserver $dbserver" if $dbserver;
+    unless ($no_update) {
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+        unless ($success) {
+            die("Unable to perform $command error code: $error_code");
+        }
+    } else {
+        print "skipping $command\n";
+    }
+}
+
+exit 0;
+
+
+sub delete_workdir {
+    my $dir = shift;
+
+    if (!-e $dir) {
+        print "outdir $dir does not exist\n";
+        return 0;
+    }
+    if (!-d $dir) {
+        print "outdir $dir is not a directory\n";
+        return 0;
+    }
+
+    # do an ls of the directory before starting
+#    print "directory listing for $dir\n";
+#    system "ls $dir";
+
+    my $command = "rm -rf $dir";
+    unless ($no_update) {
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+        unless ($success) {
+            if (-e $dir) {
+                my $rc = $error_code >> 8;
+                my_die("Unable to perform $command return status: $rc", $req_id, $rc);
+            } else {
+                print STDERR "rm claimed to fail but directory no longer exists\n";
+            }
+        }
+    } else {
+        print "skipping $command\n";
+    }
+    return 0;
+}
+
+sub my_die {
+    my $msg = shift;
+    my $req_id = shift;
+    my $fault = shift;
+
+    carp($msg);
+
+    my $command = "$pstamptool -updatereq -req_id $req_id  -set_fault $fault";
+    $command   .= " -dbname $dbname" if $dbname;
+    $command   .= " -dbserver $dbserver" if $dbserver;
+    unless ($no_update) {
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+        unless ($success) {
+            die("Unable to perform $command error code: $error_code");
+        }
+    } else {
+        print "skipping $command\n";
+    }
+    exit $fault;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_dorequest.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_dorequest.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_dorequest.pl	(revision 41103)
@@ -0,0 +1,116 @@
+#!/bin/env perl
+###
+### pstamp_dorequest.pl
+###
+###     Excecute the jobs for a given request.
+###
+###     Note: This program is not part of the postage stamp server
+###     It is intended for testing outside of pantasks environment
+###
+
+use warnings;
+use strict;
+
+if (@ARGV != 1) {
+    die "usage: $0 request_id\n";
+}
+
+my $request_id = $ARGV[0];
+
+my $verbosity = 0;
+
+use Sys::Hostname;
+my $host = hostname();
+
+if ($verbosity) {
+    print "\n\n";
+    print "Starting script $0 on $host\n\n";
+}
+
+use IPC::Cmd 0.36 qw( can_run run );
+
+use PS::IPP::Metadata::Config;
+use PS::IPP::Metadata::Stats;
+use PS::IPP::Metadata::List qw( parse_md_list );
+
+use PS::IPP::Config qw($PS_EXIT_SUCCESS
+		       $PS_EXIT_UNKNOWN_ERROR
+		       $PS_EXIT_SYS_ERROR
+		       $PS_EXIT_CONFIG_ERROR
+		       $PS_EXIT_PROG_ERROR
+		       $PS_EXIT_DATA_ERROR
+		       $PS_EXIT_TIMEOUT_ERROR
+		       metadataLookupStr
+		       metadataLookupBool
+		       caturi
+		       );
+
+my $missing_tools;
+
+my $pstamptool  = can_run('pstamptool')  or (warn "Can't find pstamptool"  and $missing_tools = 1);
+my $ppstamp_run = can_run('ppstamp_run.pl') or (warn "Can't find ppstamp_run.pl" and $missing_tools = 1);
+
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files
+
+my @psjobs;
+#Look up the jobs for the given request_id
+{
+    my $command = "$pstamptool -pendingjob";
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbosity);
+    unless ($success) {
+        die("Unable to perform pstamptool -pendingreq: $error_code");
+    }
+
+    if (@$stdout_buf == 0) {
+        print STDERR "no pending pstamp jobs found\n";
+        exit 1;
+    }
+    my $metadata = $mdcParser->parse(join "", @$stdout_buf) or
+        die("Unable to parse metdata config doc");
+
+    my $jobs = parse_md_list($metadata);
+
+    foreach my $job (@$jobs) {
+        if ($job->{req_id} == $request_id) {
+            # print STDERR "adding $job->{job_id} to the list\n";
+            $psjobs[@psjobs] = $job;
+        }
+    }
+}
+
+if (! @psjobs) {
+    # TODO: is this always an error, what if the job is no longer pending?
+    print STDERR "no pending postage stamp jobs for request $request_id found\n";
+    exit 1;
+}
+
+foreach my $job (@psjobs) {
+    my $command = "$ppstamp_run $job->{job_id}";
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbosity);
+    unless ($success) {
+        die("Unable to perform $command: $error_code");
+    }
+    ### print @$stdout_buf;
+}
+
+#
+# Update the state of the request
+#
+{
+    ## TODO: what about request status
+    my $command = "$pstamptool -updatereq -req_id $request_id -set_state stop"; 
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbosity);
+    unless ($success) {
+        die("Unable to perform pstamptool -updatereq: $error_code");
+    }
+}
+
+exit 0;
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_finish.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_finish.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_finish.pl	(revision 41103)
@@ -0,0 +1,506 @@
+#!/bin/env perl
+
+# pstamp_finish.pl
+
+use warnings;
+use strict;
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+use Carp;
+
+use Time::Local;
+use Sys::Hostname;
+use IPC::Cmd 0.36 qw( can_run run );
+use File::Temp qw( tempfile );
+use File::Copy;
+use File::Basename qw(dirname);
+
+use PS::IPP::Metadata::Config;
+use PS::IPP::Metadata::Stats;
+use PS::IPP::Metadata::List qw( parse_md_list );
+
+use PS::IPP::Config qw( :standard );
+use PS::IPP::PStamp::RequestFile qw( :standard );
+use PS::IPP::PStamp::Job qw( :standard );
+
+my %imagedb_cache;
+
+my ( $req_id, $req_name, $req_file, $outdir, $product, $dbname, $dbserver, $verbose, $save_temps, $redirect_output);
+
+# the char to the right of the bar may be used as a single - alias for the longer name
+# for example --input and -i are equivalent
+GetOptions(
+           'req_id=s'       => \$req_id,
+           'req_name=s'     => \$req_name,
+           'req_file=s'     => \$req_file,
+           'product=s'      => \$product,
+           'outdir=s'       => \$outdir,
+	   'dbname=s'       => \$dbname,
+	   'dbserver=s'     => \$dbserver,
+	   'verbose'        => \$verbose,
+	   'save-temps'     => \$save_temps,
+	   'redirect-output' => \$redirect_output,
+) or pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+
+die "usage: --req_id id --req_name name --req_file file --product product --outdir output_directory [--dbname dbname --verbose]\n"
+    if !$req_id or !$req_name or !$req_file or !$product or !$outdir;
+
+die "outdir is NULL\n" if $outdir eq "NULL";
+
+my $ipprc = PS::IPP::Config->new(); # IPP Configuration
+if ($redirect_output) {
+    # XXX: what happens here if the directory does not exist? We check below
+    my $logDest = "$outdir/psfinish.$req_id.log";
+    $ipprc->redirect_output($logDest);
+}
+
+if (!$dbserver) {
+    $dbserver =  metadataLookupStr($ipprc->{_siteConfig}, 'PS_DBSERVER');
+}
+
+my $missing_tools;
+
+my $pstamptool  = can_run('pstamptool')  or (warn "Can't find pstamptool"  and $missing_tools = 1);
+my $dsreg  = can_run('dsreg')  or (warn "Can't find dsreg"  and $missing_tools = 1);
+my $pstamp_results = can_run('pstamp_results_file.pl') 
+                            or (warn "Can't find pstamp_results_file.pl" and $missing_tools = 1);
+my $pstampdump = can_run('pstampdump') or (warn "Can't find pstampdump" and $missing_tools = 1);
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+if ($product eq "NULL") {
+    # nothing more to do
+    my_die("product is NULL!", $req_id, $PS_EXIT_PROG_ERROR);
+}
+
+my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files
+
+{
+    # set the output fileset's name to the request name.
+    my $fileset = $req_name;
+
+    print STDERR "product: $product  REQ_NAME: $req_name $outdir\n" if $verbose;
+
+    if (!-e $outdir) {
+        # something must have gone wrong parsing the request
+        print STDERR  "output directory $outdir does not exist\n";
+
+        if (!mkdir $outdir) {
+            my_die("cannot create output directory $outdir",$req_id, $PS_EXIT_UNKNOWN_ERROR);
+        }
+
+
+    } elsif (! -d $outdir ) {
+        my_die("output directory $outdir exists but is not a directory", $req_id, $PS_EXIT_UNKNOWN_ERROR);
+    }
+
+    if (! -e $req_file ) {
+        my_die("request file $req_file is missing", $req_id, $PS_EXIT_CONFIG_ERROR);
+    }
+
+    # this function is PS::IPP::PStamp::RequestFile::read_request_file
+    my ($header, $rows) = read_request_file($req_file);
+
+    if (!$header or !$rows) {
+        # Since a request got queued, the request file must have been readable at some point 
+        my_die("failed to read request file $req_file", $req_id, $PS_EXIT_CONFIG_ERROR);
+    }
+
+    # start building the list of files to be placed in the output fileset
+    my ($rlf, $reglist_name) = tempfile ("$outdir/reglist.XXXX", UNLINK => !$save_temps);
+
+    # results file
+    print $rlf "results.fits|||table|\n";
+    # human readable representation of the results file
+    print $rlf "results.mdc|||text|\n";
+
+    my $err_file = "parse_error.txt";
+    if (-e "$outdir/$err_file" ) {
+        print $rlf "$err_file|||text|\n";
+    }
+
+    my $request_fault = 0;
+    {
+        # The results table definition file
+        my ($tdf, $table_def_name) = tempfile ("$outdir/tabledef.XXXX", UNLINK => !$save_temps);
+        # data for the header
+        print $tdf "$req_name|$req_id|\n";
+        # get the list of jobs generated for this request
+        my @jobs;
+        {
+            my $command = "$pstamptool -listjob -req_id $req_id";
+            $command   .= " -dbname $dbname" if $dbname;
+            $command   .= " -dbserver $dbserver" if $dbserver;
+            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                run(command => $command, verbose => $verbose);
+            unless ($success) {
+                die("Unable to perform $command error code: $error_code");
+            }
+            my $output = join "", @$stdout_buf;
+            if (!$output) {
+                # This should not happen. A fake job should have been entered
+                my_die("Request $req_id produced no jobs", $req_id, $PS_EXIT_PROG_ERROR);
+            } else {
+                my $jobs = parse_md_fast($mdcParser, $output);
+
+                @jobs = @$jobs;
+            }
+        }
+
+        my $exp_info;
+        foreach my $job (@jobs) {
+            my $job_id = $job->{job_id};
+            my $job_type = $job->{jobType};
+            my $rownum = $job->{rownum};
+            my $fault = $job->{fault};
+            my $exp_id = $job->{exp_id};
+
+            my $error_string = get_error_string($fault);
+
+            if (($fault eq $PSTAMP_DUP_REQUEST) and ($req_name eq "NULL")) {
+                # this request had a duplicate request name yet the parser didn't give
+                # it an "ERROR style name.
+                my_die("duplicate request not given a request name", $req_id, $PS_EXIT_PROG_ERROR);
+            }
+            my ($row, $req_info, $project) = get_request_info($rows, $rownum);
+
+            my $proj_hash = resolve_project($ipprc, $project, $dbname, $dbserver);
+            my $image_db = $proj_hash->{dbname};
+            if (!$image_db and !$fault) {
+                # if project isn't resolvable, the paser should have faulted this job
+                my_die("failed to find imagedb for project $project", $req_id, $PS_EXIT_CONFIG_ERROR);
+            }
+
+            my $job_params = get_job_parameters($job);
+            my $stage = "";
+            if ($job_params) {
+                $stage = $job_params->{stage};
+            }
+
+            if ($stage ne 'stack') {
+                # get the metadata for the exposure (if any i.e. stack) 
+                # returns an appropriate string if !$exp_id
+                $exp_info = get_exposure_info($job_params, $image_db, $exp_id);
+            } else {
+                my $filter = $job_params->{filter};
+                $filter = "0" if !$filter;
+                my $mjd_obs = $job_params->{mjd_obs};
+                $mjd_obs = "0" if !$mjd_obs;
+                $exp_info = "$mjd_obs|0|0|$filter|0|0";
+            }
+
+            if (($job_type eq "stamp") || ($job_type eq "get_image") || ($job_type eq "none")) {
+                my $jreglist = "$outdir/reglist$job_id";
+                if (open JRL, "<$jreglist") {;
+                    # process the reglist file to get the list of files produced by this job
+                    foreach my $line (<JRL>) {
+
+                        # XXX: we are getting many cases where the size and/or md5sum calculated by
+                        # the job has changed by the time the request_finish has run
+                        # Don't
+                        # add line to the requests's reglist
+                        # ....
+    #                    print $rlf $line;
+
+
+                        chomp $line;
+                        my ($img_name, $reported_size, $reported_sum, $filetype) = split '\|', $line;
+                        my $use_supplied_size = 1;
+                        if ($use_supplied_size) {
+                            print $rlf "$img_name|$reported_size|$reported_sum|$filetype|\n";
+                        } else {
+                            # ... instead let dsreg compute the paramters by leaving them blank
+                            print $rlf "$img_name|||$filetype|\n";
+                        }
+
+                        # add line to the table definition file
+                        print $tdf "$rownum|$fault|$error_string|$img_name|$job_id|";
+
+                        # ra_deg and dec_deg are the coordinates of center of the stamp
+                        # first assume that the image is compressed and check the first extension.
+                        # If not found check the PHU. If that doesn't work just set them to zero.
+                        # XXX do this more cleanly
+                        my (undef, $ra_deg, $dec_deg) = split " ", `echo $outdir/$img_name | fields -x 0 RA_DEG DEC_DEG`;
+                        if (!defined $ra_deg) {
+                            (undef, $ra_deg, $dec_deg) = split " ", `echo $outdir/$img_name | fields RA_DEG DEC_DEG`;
+                        }
+                        $ra_deg = 0.0 if (!$ra_deg);
+                        $dec_deg = 0.0 if (!$dec_deg);
+                        print $tdf "$ra_deg|$dec_deg|";
+
+                        print $tdf "$exp_info|";
+                        print $tdf "$req_info|";
+                        print $tdf "\n";
+                    }
+                    close JRL;
+                } else {
+                    my_die("No reglist for successful job: $job_id", $req_id, $PS_EXIT_PROG_ERROR) 
+                        if $job->{state} eq 'stop' and $fault eq $PSTAMP_SUCCESS;
+                    print STDERR "no reglist file for job $job_id\n" if $verbose;
+                    print $tdf "$rownum|$fault|$error_string|0|$job_id|";
+                    print $tdf "0|0|";       # center of (non-existent) stamp
+                    print $tdf "$exp_info|";
+                    print $tdf "$req_info|";
+                    print $tdf "\n";
+                }
+            } else {
+                # XXX do list jobs
+                # we can probably arange things to use the code as above and skip the fileset registration
+                print STDERR "Unknown jobType: $job_type";
+                next;
+            }
+        }
+        close $tdf;
+        # make the results file
+        {
+            my $command = "$pstamp_results --input $table_def_name --output $outdir/results.fits";
+            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                run(command => $command, verbose => $verbose);
+            unless ($success) {
+                print STDERR "Unable to perform $command error code: $error_code\n";
+                $request_fault = $error_code >> 8;
+            } else {
+                # dump a textual representation
+                my $command = "$pstampdump $outdir/results.fits > $outdir/results.mdc";
+                my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                    run(command => $command, verbose => $verbose);
+                unless ($success) {
+                    $request_fault = $error_code >> 8;
+                    my_die("Unable to perform $command error code: $error_code", $req_id, $request_fault);
+                }
+            }
+        }
+    }
+
+    close $rlf;
+
+    if (!$request_fault) {
+        # register the fileset
+        my $command = "$dsreg --list $reglist_name --add $fileset --product $product --type PSRESULTS";
+        $command .= " --link --datapath $outdir --ps0 $req_id";
+
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+        unless ($success) {
+            $request_fault = $error_code >> 8;
+            my_die("Unable to perform $command error code: $error_code\n", $req_id, $request_fault);
+        }
+    }
+    # set the request's state to stop
+    {
+        my $command = "$pstamptool -updatereq -req_id $req_id -set_state stop";
+        $command   .= " -dbname $dbname" if $dbname;
+        $command   .= " -dbserver $dbserver" if $dbserver;
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+        unless ($success) {
+            die("Unable to perform $command error code: $error_code");
+        }
+    }
+    exit $request_fault;
+}
+
+sub my_die {
+    my $msg = shift;
+    my $req_id = shift;
+    my $fault  = shift;
+
+    carp($msg);
+
+    my $command = "$pstamptool -updatereq -req_id $req_id -set_fault $fault";
+    $command   .= " -dbname $dbname" if $dbname;
+    $command   .= " -dbserver $dbserver" if $dbserver;
+
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        die("Unable to perform $command error code: $error_code");
+    }
+    exit $fault;
+}
+
+sub get_request_info {
+    my $rows = shift;
+    my $rownum = shift;
+
+    if ($rownum eq 0) {
+        my $dummy_rowinfo = "0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|";
+        return (undef, $dummy_rowinfo, "none");
+    }
+    my $row = $rows->{$rownum};
+
+
+    # these may be set to null during processing
+    my $component = $row->{COMPONENT};
+    $component = "null" if !$component;
+    my $tess_id = $row->{TESS_ID};
+    $tess_id = "null" if !$tess_id;
+    my $comment = $row->{COMMENT};
+    $comment = "null" if !$comment;
+    my $data_group = $row->{DATA_GROUP};
+    if (!defined $data_group) {
+        # XXX: backwards compatibility hook. Remove "soon".
+        $data_group = $row->{LABEL};
+    }
+    $data_group = "null" if !$data_group;
+
+    # XXX: This is ugly, error prone and hard to change.
+    # Create a results file module and provide a list of the names (we have the data in the columns)
+    my $rowinfo = "$row->{PROJECT}|$row->{JOB_TYPE}|$row->{REQ_TYPE}|$row->{IMG_TYPE}|";
+    $rowinfo   .= "$row->{ID}|$tess_id|$component|$data_group|$row->{OPTION_MASK}|$row->{MJD_MIN}|$row->{MJD_MAX}|";
+    $rowinfo   .= "$row->{REQFILT}|$row->{COORD_MASK}|$row->{CENTER_X}|$row->{CENTER_Y}|";
+    $rowinfo   .= "$row->{WIDTH}|$row->{HEIGHT}|";
+    $rowinfo   .= $comment;
+
+    return ($row, $rowinfo, $row->{PROJECT});
+}
+
+sub get_job_parameters {
+    my $job = shift;
+    if (!$job->{outputBase}) {
+        print "get_job_parameters: $job->{job_id} has no outputBase\n";
+        return undef;
+    }
+    my $params_file = $job->{outputBase} . '.mdc';
+    if (! -e $params_file ) {
+        print "get_job_parameters: $job->{job_id} has no parameters file\n";
+        return undef;
+    }
+    open IN, "<$params_file" or die "unable to open $params_file";
+    my $data = join "", (<IN>);
+    close IN;
+    if (! $data ) {
+        print "get_job_parameters: parameters file is empty\n";
+        return undef;
+    }
+    my $metadata = $mdcParser->parse($data) or die("Unable to parse metdata config doc");
+
+    # no need to use parse_md_fast here
+    my $results = parse_md_list($metadata);
+    if (scalar @$results != 1) {
+        print STDERR "get_job_params: failed to parse_md_list\n";
+        return undef;
+    }
+    return $results->[0];
+}
+
+sub get_exposure_info {
+    my $job_params = shift;
+    my $image_db= shift;
+    my $exp_id = shift;
+
+    my ($dateobs, $ra, $decl, $filter, $exp_time, $exp_name);
+
+    $dateobs  = $job_params->{dateobs};
+    $ra       = $job_params->{ra};
+    $decl     = $job_params->{decl};
+    $filter   = $job_params->{filter};
+    $exp_time = $job_params->{exp_time};
+    $exp_name = $job_params->{exp_name};
+
+    unless (defined $dateobs and defined $ra and defined $decl and defined $filter and defined $exp_time and defined $exp_name) {
+        # job params don't have all of the values that we need (most likely this is a diff stage job)
+        # go look up the exposure if we have one
+        if (!$exp_id or !$image_db) {
+            # no exposure id just return zeros
+            # XXX: we could put a value in for filter, but we don't have a good place to find it
+
+            #"$mjd_obs|$ra_obs|$dec_obs|$filter|$exp_time|$fpa_id";
+            return "0|0|0|0|0|0";
+        }
+
+        my $exp;
+        # get cache of exposures for this image_db
+        my $exp_cache = $imagedb_cache{$image_db};
+        if (defined $exp_cache) {
+            $exp = $exp_cache->{$exp_id};
+        }
+
+        unless (defined $exp) {
+
+            my $regtool = can_run('regtool') or (warn "Can't find regtool" and $missing_tools = 1);
+            if ($missing_tools) {
+                warn("Can't find required tools.");
+                exit ($PS_EXIT_CONFIG_ERROR);
+            }
+
+            my $command = "$regtool -processedexp -dbname $image_db -exp_id $exp_id";
+
+            # run the tool and parse the output
+            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                        run(command => $command, verbose => $verbose);
+            unless ($success) {
+                # not sure if we should die here
+                die "cannot get exposure information for $exp_id from image database $image_db";
+            }
+            my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files
+
+            my $output = join "", @$stdout_buf;
+            if (!$output) {
+                print STDERR "no output returned from $command\n" if $verbose;
+                return undef;
+            }
+            my $exposures = parse_md_fast($mdcParser, $output);
+            my $numExp = @$exposures;
+
+            die "unexpected number of exposures $numExp found for exp_id: $exp_id in DB: $image_db" if $numExp != 1;
+
+            $exp = $exposures->[0];
+
+            unless (defined $exp_cache) {
+                my %new_exp_cache;
+                $exp_cache = \%new_exp_cache;
+                $imagedb_cache{$image_db} = $exp_cache;
+            }
+            $exp_cache->{$exp_id} = $exp;
+        } else {
+            print "found $exp_id in cache\n";
+        }
+        
+        #my $info = "$mjd_obs|$ra_obs|$dec_obs|$filter|$exp_time|$fpa_id";
+        $dateobs  = $exp->{dateobs};
+        $ra       = $exp->{ra};
+        $decl     = $exp->{decl};
+        $filter   = $exp->{filter};
+        $exp_time = $exp->{exp_time};
+        $exp_name = $exp->{exp_name};
+    }
+
+    die "failed to find exp_info for $exp_id" unless (defined $dateobs and defined $ra and defined $decl and defined $filter and defined $exp_time and defined $exp_name);
+    
+    use constant RADIANS_TO_DEGREES => 90. / atan2(1, 0);
+    my $ra_deg   = $ra * RADIANS_TO_DEGREES;
+    my $decl_deg = $decl * RADIANS_TO_DEGREES;
+    my $mjd_obs = dateobs_to_mjd($dateobs);
+
+    my $info = "$mjd_obs|$ra_deg|$decl_deg|$filter|$exp_time|$exp_name";
+            
+    return $info;
+}
+
+sub dateobs_to_mjd {
+    my $dateobs = shift;
+
+    # dateobs is in format: 1970-01-01T00:00:00
+
+    my ($date, $time) = split "T", $dateobs;
+    my ($year, $mon, $day) = split "-", $date;
+    my ($hr, $min, $sec) = split ":", $time;
+
+    my $ticks = timegm($sec, $min, $hr, $day, $mon-1, $year-1900);
+
+    # dateobs is UTC convert to TAI
+    # XXX: Do this properly
+    if ($year >= 2009) {
+        $ticks += 34;
+    } else {
+        $ticks += 33;
+    }
+
+    return 40587.0 + ($ticks/86400.);
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_get_image_job.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_get_image_job.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_get_image_job.pl	(revision 41103)
@@ -0,0 +1,214 @@
+#!/usr/bin/env perl
+#
+# Execute a get_image job for the postage stamp server
+#
+
+use strict;
+use warnings;
+
+use Getopt::Long qw( GetOptions );
+use Pod::Usage qw( pod2usage );
+use DBI;
+use PS::IPP::Metadata::Config;
+use PS::IPP::Metadata::Stats;
+use PS::IPP::Metadata::List qw( parse_md_list );
+use File::Copy;
+use File::Basename;
+use Digest::MD5::File qw( file_md5_hex );
+use IPC::Cmd 0.36 qw( can_run run );
+
+
+use PS::IPP::Config qw( :standard );
+
+my $product;
+my $fileset;
+
+my $output_base;
+my $bundleroot;
+
+my $verbose;
+my $imagedbname;
+my $dbname;
+my $dbserver;
+my $job_id;
+my $rownum;
+
+#
+# parse args
+#
+
+GetOptions(
+        'job_id=s'        =>      \$job_id,
+        'rownum=s'        =>      \$rownum,
+        'output_base=s'   =>      \$output_base,
+        'bundleroot=s'    =>      \$bundleroot,
+        'imagedbname=s'   =>      \$imagedbname,
+        'dbname=s'        =>      \$dbname,
+        'dbserver=s'      =>      \$dbserver,
+        'verbose'         =>      \$verbose,
+) or pod2usage(2);
+
+my $err = "";
+$err .= "--job_id is required\n" if (!$job_id);
+$err .= "--rownum is required\n" if (!$rownum);
+$err .= "--output_base is required to specify the output fileset\n" if (!$output_base);
+
+my_die( $err, $PS_EXIT_PROG_ERROR) if $err;
+
+my $ipprc = PS::IPP::Config->new();
+
+my $params_file = $output_base . ".mdc";
+if (! open(INPUT, "<$params_file") ) {
+    my_die("failed to open params file: $params_file", $PS_EXIT_UNKNOWN_ERROR);
+}
+
+my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files
+
+my $data = $mdcParser->parse(join "", (<INPUT>)) 
+    or my_die("failed to parse metadata config doc", $PS_EXIT_UNKNOWN_ERROR);
+
+my $components = parse_md_list($data);
+my $n = scalar @$components;
+if ($n != 1) {
+    my_die("params file $params_file contains unexpected number of components: $n", $PS_EXIT_PROG_ERROR);
+}
+my $comp = $components->[0];
+my $stage = $comp->{stage};
+my $stage_id = $comp->{stage_id};
+my $component = $comp->{component};
+my $path_base = $comp->{path_base};
+my $camera = $comp->{camera};
+my $magicked = $comp->{magicked};
+
+if ($verbose) {
+    print STDERR "\nstage is $stage\n";
+    print STDERR "stage_id is $stage_id\n";
+    print STDERR "path_base is $path_base\n";
+    print STDERR "CAMERA is $camera\n";
+    print STDERR "magicked is " . (defined $magicked ? $magicked : "undefined") . "\n";
+}
+
+if (!$camera or !$path_base or !$component or !$stage_id or !$stage) {
+       my_die("One or more parameters are missing in: $params_file", $PS_EXIT_UNKNOWN_ERROR);
+}
+
+
+# Look for programs we need
+my $missing_tools;
+my $dist_bundle   = can_run('dist_bundle.pl') or (warn "Can't find dist_bundle.pl" and $missing_tools = 1);
+my $pstamptool   = can_run('pstamptool') or (warn "Can't find pstamptool" and $missing_tools = 1);
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit($PS_EXIT_CONFIG_ERROR);
+}
+
+$pstamptool .= " -dbname $dbname" if $dbname;
+$pstamptool .= " -dbserver $dbserver" if $dbserver;
+
+my $outdir = dirname($output_base);
+my $basename = basename($path_base);
+my $results_file = $output_base . ".bundle_results";
+my $outroot;
+if ($bundleroot) {
+    my (undef, undef, undef, $mday, $month, $year) = gmtime(time());
+    $month += 1;
+    $year += 1900;
+
+    # This will generate an outroot like:
+    # neb://any/pstamp/bundles/2011/11/22/14026916_1_1_o5739g0358o.353654.wrp.215092.skycell.2083.025
+
+    $outroot = $bundleroot . sprintf("/%4d/%02d/%02d/${job_id}_", $year, $month, $mday) . basename($output_base) . "_". $basename;
+} else {
+    $outroot = $output_base ."_" . $basename;
+}
+
+{
+    my $command = "$dist_bundle --camera $camera --stage $stage --stage_id $stage_id";
+    $command .= " --results_file $results_file";
+    $command .= " --component $component --path_base $path_base --outroot $outroot";
+    # DING DONG ....
+    $command .= " --no_magic";
+    $command .= " --dbname $imagedbname" if $imagedbname;
+    $command .= " --verbose" if $verbose;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        my_die("Unable to perform $command: $error_code", $error_code >> 8);
+    }
+}
+
+if (! open(RESULTS, "<$results_file")) {
+    my_die("failed to open bundle results file: $results_file", $PS_EXIT_UNKNOWN_ERROR);
+}
+
+my $file_name;
+my $bytes;
+my $md5sum;
+foreach my $line (<RESULTS>) {
+    chomp $line;
+    next if !$line;
+    next if $line =~ /bundleResults/;
+    last if $line =~ /END/;
+
+    my ($tag, $type, $val) = split " ", $line;
+    if ($tag eq "name") {
+        $file_name = $val;
+    } elsif ($tag eq "bytes") {
+        $bytes = $val;
+    } elsif ($tag eq "md5sum") {
+        $md5sum = $val;
+    } else {
+        my_die("unexpected tag: $tag  found in results file: $results_file", $PS_EXIT_PROG_ERROR);
+    }
+}
+
+my $reglist = "$outdir/reglist$job_id";
+if (! open(REGLIST, ">$reglist") ) {
+    my_die("failed to open registration list: $reglist", $PS_EXIT_UNKNOWN_ERROR);
+}
+
+if ($bundleroot) {
+    {
+        # delete any existing pstampFile in case this job has faulted and
+        # been reverted
+        my $command = "$pstamptool -deletefile -job_id $job_id";
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+        unless ($success) {
+            my_die("Unable to perform $command: $error_code", $error_code >> 8);
+        }
+    }
+    my $linkname = "$outdir/$file_name";
+    if (-l $linkname or -e $linkname) {
+        unlink $linkname or my_die("Failed to unlink existing symlink: $linkname", $PS_EXIT_UNKNOWN_ERROR);
+    }
+    my $bundle_name = dirname($outroot) . "/$file_name";
+    my $resolved = $ipprc->file_resolve($bundle_name);
+    if (!$resolved) {
+        my_die("failed to resolve $bundle_name", $PS_EXIT_UNKNOWN_ERROR);
+    }
+    symlink $resolved, $linkname or my_die("failed to create symlink to $resolved in $outdir", 
+        $PS_EXIT_UNKNOWN_ERROR);
+
+    my $command = "$pstamptool -addfile -job_id $job_id -path $bundle_name";
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        my_die("Unable to perform $command: $error_code", $error_code >> 8);
+    }
+}
+
+
+print REGLIST "$file_name|$bytes|$md5sum|tgz|\n";
+
+close(REGLIST);
+
+exit 0;
+
+sub my_die {
+    my $msg = shift;
+    my $rc = shift;
+
+    print STDERR $msg;
+    exit $rc ? $rc : $PS_EXIT_UNKNOWN_ERROR;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_insert_request.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_insert_request.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_insert_request.pl	(revision 41103)
@@ -0,0 +1,190 @@
+#!/bin/env perl
+###
+#
+# create a postage stamp request
+#
+###
+
+use warnings;
+use strict;
+
+use Carp;
+use Getopt::Long qw( GetOptions );
+use Sys::Hostname;
+use File::Copy;
+use POSIX qw( strftime );
+
+my $host = hostname();
+my $verbose = 0;
+my $dbname;
+my $dbserver;
+my $tmp_req_file;
+my $workdir;
+
+GetOptions(
+    'tmp_req_file=s'=>  \$tmp_req_file,
+    'workdir=s'     =>  \$workdir,
+    'dbname=s'      =>  \$dbname,
+    'dbserver=s'    =>  \$dbserver,
+    'verbose'       =>  \$verbose,
+);
+
+die "required arguments --tmp_req_file --workdir --dbname --dbserver"
+    if( !defined($tmp_req_file) or !defined($workdir) or !defined ($dbname)
+        or !defined($dbserver));
+use IPC::Cmd 0.36 qw( can_run run );
+
+use PS::IPP::Config qw( :standard );
+
+use Cwd;
+
+my $cwd = cwd();
+
+my $missing_tools;
+
+my $pstamptool = can_run('pstamptool')  or (warn "Can't find pstamptool"  and $missing_tools = 1);
+my $fields = can_run('fields')  or (warn "Can't find fields"  and $missing_tools = 1);
+my $fhead = can_run('fhead')  or (warn "Can't find fhead"  and $missing_tools = 1);
+
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+my ($extname, $extver, $req_name);
+{
+    # get the header keywords of interest.
+    # Note that if it's a pstamp request then REQ_NAME should be defined.
+    # if it's a detectability query it will not have a REQ_NAME but will have a QUERY_ID
+    # my $command = "echo $tmp_req_file | $fields -x 0 EXTNAME EXTVER REQ_NAME QUERY_ID";
+    my $command = "$fhead -x 0 $tmp_req_file";
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        print STDERR @$stderr_buf;
+        exit $error_code >> 8;
+    }
+    my $output = join "", @$stdout_buf;
+
+
+    my $makehash = 0;
+    my %hash;
+    foreach my $line (split "\n", $output) {
+        chomp $line;
+        # split lines inte left and right using equals sign. left is the keyword
+        my ($key, $right) = split "=", $line;
+            # skip if there was no '='
+        next if !$right;
+
+        $key =~ s/ //g;
+
+        # separate value from comment
+        my ($value, $comment) = split "/", $right;
+        # remove ' and space characters from key and value
+        $value =~ s/'//g;
+        $value =~ s/ //g;
+
+        #    print "$key $value\n";
+
+        # extract the values that we are looking for
+        $extname  = $value if ($key eq "EXTNAME");
+        $extver   = $value if ($key eq "EXTVER");
+        $req_name = $value if ($key eq "REQ_NAME");
+        $req_name = $value if ($key eq "QUERY_ID");
+
+        # optionally build hash. Duplicate keys get the last value seen
+        if ($makehash) {
+            $hash{$key} = $value;
+        }
+    }
+
+    # (undef, $extname, $extver, $req_name) = split " ", $output;
+
+    if (!$extname or ! (($extname eq "PS1_PS_REQUEST") or ($extname eq "MOPS_DETECTABILITY_QUERY"))) {
+        print STDERR "invalid request file\n";
+        print "invalid request file\n";
+        exit $PS_EXIT_DATA_ERROR;
+    }
+
+    if (!defined $req_name) {
+        print STDERR "invalid request file no REQ_NAME or QUERY_ID\n";
+        print "invalid request file no REQ_NAME or QUERY_ID\n";
+        exit $PS_EXIT_DATA_ERROR;
+    }
+    if (!defined $extver) {
+        print STDERR "invalid request file no EXTVER found\n";
+        print "invalid request file no EXTVER found\n";
+        exit $PS_EXIT_DATA_ERROR;
+    }
+}
+
+# put file in directory for the current date
+my $datestr = strftime "%Y/%m/%d", gmtime;
+my $datedir = "$workdir/webreq/$datestr";
+if (! -e $datedir ) {
+    my $rc = system "mkdir -p $datedir";
+    if ($rc) {
+        my $status = $rc >> 8;
+        print STDERR  "failed to create working directory $datedir: $rc $status";
+        exit $PS_EXIT_CONFIG_ERROR;
+    }
+}
+
+
+my $webreq_num = get_webreq_num();
+my $req_file = "$datedir/web_$webreq_num.fits";
+
+if (!copy( $tmp_req_file, $req_file)) {
+    die("Unable to copy request file $tmp_req_file to $req_file");
+}
+
+# Queue the request
+my $req_id = 0;
+{
+    my $command = "$pstamptool -addreq -uri $req_file -ds_id 0 -name $req_name";
+    $command .= " -dbname $dbname" if $dbname;
+    $command .= " -dbserver $dbserver" if $dbserver;
+    $command .= " -label WEB.UP";
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        my $errbuf = join "", @$stderr_buf;
+        print STDERR $errbuf;
+        if ($errbuf =~ /Duplicate entry/) {
+            print "Request Name $req_name has already been used";
+            exit $PS_EXIT_DATA_ERROR;
+        } else {
+            exit $PS_EXIT_UNKNOWN_ERROR;
+        }
+    }
+    $req_id = ${$stdout_buf}[0];
+}
+
+print "$req_id $req_name\n";
+
+exit 0;
+
+# Ask the database for the next web request number
+sub get_webreq_num
+{
+    my $command = "$pstamptool -getwebrequestnum";
+    $command .= " -dbname $dbname" if $dbname;
+    $command .= " -dbserver $dbserver" if $dbserver;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+	my $status = $error_code >> 8;
+        print STDERR @$stderr_buf;
+        die("Unable to perform $command: $error_code : $status");
+    }
+    my $webreq_num = ${$stdout_buf}[0];
+    chomp $webreq_num;
+
+    if (!$webreq_num) {
+        die("pstamptool -getwebreqnum returned no value");
+    }
+
+    # print STDERR "webreq_num $webreq_num\n";
+
+    return $webreq_num;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_job_run.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_job_run.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_job_run.pl	(revision 41103)
@@ -0,0 +1,942 @@
+#!/bin/env perl
+###
+### pstamp_job_run.pl
+###
+###     Run a given postage stamp Job
+###
+
+use warnings;
+use strict;
+
+use Sys::Hostname;
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Carp;
+use File::Basename;
+use File::Copy;
+use File::Temp qw(tempfile tempdir);
+use Digest::MD5::File qw( file_md5_hex );
+use PS::IPP::PStamp::RequestFile qw( :standard );
+use PS::IPP::PStamp::Job qw( :standard );
+use IPC::Cmd 0.36 qw( can_run run );
+use POSIX;
+
+use PS::IPP::Metadata::Config;
+#use PS::IPP::Metadata::Stats;
+use PS::IPP::Metadata::List qw( parse_md_list );
+
+use PS::IPP::Config qw( :standard );
+
+my ($job_id, $redirect_output, $outputBase, $rownum, $jobType, $options); 
+my ($verbose, $dbname, $dbserver, $no_update, $save_temps);
+my $very_verbose = 0; # for debugging
+
+GetOptions(
+    'job_id=s'          =>  \$job_id,
+    'job_type=s'        =>  \$jobType,
+    'rownum=s'          =>  \$rownum,
+    'output_base=s'     =>  \$outputBase,
+    'options=s'         =>  \$options,
+    'redirect-output'   =>  \$redirect_output,
+    'dbname=s'          =>  \$dbname,
+    'dbserver=s'        =>  \$dbserver,
+    'verbose'           =>  \$verbose,
+    'no-update'         =>  \$no_update,
+    'save-temps'        =>  \$save_temps,
+);
+
+
+my $host = hostname();
+if ($verbose) {
+    print "\n\n";
+    print "Starting script $0 on $host\n\n";
+}
+
+die "job_id is required" if !$job_id;
+
+my_die( "job_type is required", $job_id, $PS_EXIT_PROG_ERROR) if !$jobType;
+my_die("rownum is required", $job_id, $PS_EXIT_PROG_ERROR) if !$rownum;
+my_die("output_base is required", $job_id, $PS_EXIT_PROG_ERROR) if !$outputBase;
+
+# ppstamp requires an input file
+$options = $PSTAMP_SELECT_IMAGE if !$options;
+
+my $ipprc = PS::IPP::Config->new(); # IPP Configuration
+if ($redirect_output) {
+    my $logDest = "$outputBase.log";
+    $ipprc->redirect_output($logDest)
+        or my_die ("unable to redirect output to $logDest", $job_id, $PS_EXIT_UNKNOWN_ERROR);
+}
+
+if ($verbose && $save_temps) {
+    # we're probably debugging turn up the verbosity
+    $very_verbose = 1;
+}
+
+if (!$dbserver) {
+    $dbserver =  metadataLookupStr($ipprc->{_siteConfig}, 'PS_DBSERVER');
+}
+
+my $temproot = metadataLookupStr($ipprc->{_siteConfig}, "TEMP.DIR");
+$temproot = "/tmp" if !defined $temproot;
+
+my $missing_tools;
+
+my $pstamptool = can_run('pstamptool')  or (warn "Can't find pstamptool"  and $missing_tools = 1);
+my $ppstamp = can_run('ppstamp') or (warn "Can't find ppstamp" and $missing_tools = 1);
+my $pstamp_get_image_job = can_run('pstamp_get_image_job.pl') or (warn "Can't find pstamp_get_image_job.pl" and $missing_tools = 1);
+my $psgetcalibinfo = can_run('psgetcalibinfo') or (warn "Can't find psgetcalibinfo" and $missing_tools = 1);
+my $dquery_job_run = can_run('dquery_job_run.pl') or (warn "Can't find dquery_job_run.pl" and $missing_tools = 1);
+my $whichnode = can_run('whichnode') or (warn "can't find whichnode" and $missing_tools = 1);
+my $ppBackground = can_run('ppBackground') or (warn "Can't find ppBackground" and $missing_tools = 1);
+my $ppBackgroundStack = can_run('ppBackgroundStack') or (warn "Can't find ppBackgroundStack" and $missing_tools = 1);
+my $stack_bkg_mk_mdc = can_run('stack_bkg_mk_mdc.pl') or (warn "Can't find stack_bkg_mk_mdc.pl" and $missing_tools = 1);
+my $fpack = can_run('fpack') or (warn "Can't find fpack" and $missing_tools = 1);
+my $staticskytool = can_run('staticskytool') or (warn "Can't find staticskytool" and $missing_tools = 1);
+
+if ($missing_tools) {
+    my_die("Can't find required tools", $job_id, $PS_EXIT_CONFIG_ERROR);
+}
+
+my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files
+
+
+my $jobStatus;
+if ($jobType eq "stamp") {
+    my $params = read_params_file($outputBase);
+
+    my $argString;
+    $argString = $params->{job_args};
+    
+    my_die("argument list is empty", $job_id, $PS_EXIT_DATA_ERROR) if !$argString;
+
+    my $stage = $params->{stage};
+    my_die("stage is not defined", $job_id, $PS_EXIT_DATA_ERROR) if !$stage;
+
+    if ($stage eq 'stack_summary') {
+
+        # remove options not supported by stack summary
+        $options &= ~($PSTAMP_SELECT_SOURCES | $PSTAMP_SELECT_BACKMDL | $PSTAMP_SELECT_INVERSE 
+            | $PSTAMP_RESTORE_BACKGROUND);
+
+        # stackSummary outputs do not follow the usual IPP conventions for file names.
+        # The parser (actually Job.pm) has deferred handling this until here
+        update_stack_summary_filenames($params);
+
+    } elsif ($stage ne 'stack') {
+        # ignore options only supported by stack and stack_summary
+        $options &= ~($PSTAMP_SELECT_EXP | $PSTAMP_SELECT_NUM);
+    }
+   
+
+    if ($stage eq "raw") {
+        # zap options that don't apply to raw stage
+        $options &= ~($PSTAMP_SELECT_MASK | $PSTAMP_SELECT_VARIANCE); 
+    }
+
+    if ($options & $PSTAMP_RESTORE_BACKGROUND) {
+        #  this subroutine creates a background restored version of the image or fails.
+        # upon success params->{image} is replaced with the new (temporary) version
+        my $error_code = create_background_restored_images($params);
+        if ($error_code) {
+            # if error code is one of the permanant ones set state of job to stop
+            my_die("failed to create background restored images", $job_id, $error_code, 
+                $error_code >= $PSTAMP_FIRST_ERROR_CODE ? 'stop' : undef);
+        }
+    }
+
+    my $image = $params->{image};
+    my $mask;
+    my $variance;
+
+    my $fileArgs = " -file $params->{image}";
+    my @file_list = ($params->{image});
+    
+    if ($options & $PSTAMP_SELECT_MASK) {
+        $mask = $params->{mask};
+        $fileArgs .= " -mask $mask";
+        push @file_list, $mask;
+    }
+    if ($options & $PSTAMP_SELECT_VARIANCE) {
+        $variance = $params->{weight};
+        $fileArgs .= " -variance $variance";
+        push @file_list, $variance;
+    }
+
+    if ($params->{astrom}) {
+        $argString .= " -astrom $params->{astrom}";
+        push @file_list, $params->{astrom};
+    }
+
+    if ($options & $PSTAMP_SELECT_SOURCES) {
+        # Extract sources from astrometry file if provided. This will be the smf for chip stage
+        # or the skycal cmf for stacks
+        if ($params->{astrom}) {
+            $argString .= " -write_cmf";
+            if ($stage eq 'stack') {
+                # Set psphot recipe to STACKPHOT so that the extended source paramters will
+                # be copied from the cmf file.
+                $argString .= " -recipe PSPHOT STACKPHOT"
+            }
+        } elsif ($stage eq 'stack') {
+            # no astrom file for stack (skycal cmf) Go find a staticsky cmf and use that
+            # otherwise silently ignore the request for sources since the stack smf is not useful.
+            my $staticsky_cmf = findStaticskyCMF($params);
+            if ($staticsky_cmf) {
+                print "Using staticsky cmf file $staticsky_cmf for sources\n";
+                $argString .= " -write_cmf";
+                $fileArgs  .= " -sources $staticsky_cmf";
+                # Set psphot recipe to STACKPHOT so that the extended source paramters will
+                # be copied from the cmf file.
+                $argString .= " -recipe PSPHOT STACKPHOT";
+                push @file_list, $staticsky_cmf;
+            } else {
+                print "No skycal or staticsky cmf for $params->{stack_id} ignoring request for sources\n";
+            }
+        } elsif ($params->{cmf}) {
+            $argString .= " -write_cmf";
+            $fileArgs  .= " -sources $params->{cmf}";
+            push @file_list, $params->{cmf};
+        } else {
+            print "Could not find suitable sources file will not write cmf\n";
+        }
+    }
+
+    # check that actual input files exist
+    check_files($PSTAMP_NOT_AVAILABLE, @file_list);
+
+    # find our output directory
+    my $outdir = dirname($outputBase);
+
+    my ($calib_fd, $calibfile);
+    if ($stage eq 'chip' or $stage eq 'warp') {
+        my $cam_id = $params->{cam_id};
+        if (!$cam_id) {
+            carp "no cam_id found in job params\n";
+            exit $PS_EXIT_PROG_ERROR;
+        }
+        ($calib_fd, $calibfile) = tempfile ("$outdir/calib.XXXX", UNLINK => !$save_temps);
+        close $calib_fd;
+
+        my $command = "$psgetcalibinfo --cam_id $cam_id --output $calibfile --dbname $params->{imagedb}";
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+
+        my $exitStatus;
+        if (WIFEXITED($error_code)) {
+            $exitStatus = WEXITSTATUS($error_code);
+        } else {
+            print STDERR "psgetcalibinfo failed error_code: $error_code\n";
+            $exitStatus = $PS_EXIT_SYS_ERROR;
+        }
+        exit $exitStatus if $exitStatus;
+
+        if (-s $calibfile == 0) {
+            print "no calibration information found for $cam_id\n";
+            $calibfile = undef;
+        }
+    }
+
+    # unless the stage is stack_summary we use ppstamp to make postage stamps (including -wholefile)
+    # if stage is stack_summary we make copies of the input files to the outputs
+    my $use_ppstamp = ($stage ne 'stack_summary');
+
+    my $exitStatus;
+    if ($use_ppstamp) {
+        my $command = "$ppstamp $outputBase $argString $fileArgs";
+        $command .= " -write_jpeg" if ($options & $PSTAMP_SELECT_JPEG);
+        $command .= " -nocompress" if ($options & $PSTAMP_SELECT_UNCOMPRESSED);
+        $command .= " -stage $stage";
+        $command .= " -forheader $calibfile" if $calibfile;
+	## MEH hack for centeroffchip -- needs constraint for coord_mask 0 all in sky values..
+	$command .= " -centeroffchip" if ($options & $PSTAMP_MULTI_OVERLAP_IMAGE);
+
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+
+        if (WIFEXITED($error_code)) {
+            $exitStatus = WEXITSTATUS($error_code);
+        } else {
+            print STDERR "ppstamp failed error_code: $error_code\n";
+            $exitStatus = $PS_EXIT_SYS_ERROR;
+        }
+
+        # if stage is stack deal with EXP and NUM images if selected by running ppstamp again
+        if (!$exitStatus and $stage eq 'stack' and ($options & ($PSTAMP_SELECT_EXP | $PSTAMP_SELECT_NUM))) {
+            # XXX Here I am assuming that nothing relevant gets added to $argString 
+            # Need to check
+            my $roiArgs = $params->{job_args};
+
+            # XXX: define expnum and exptime input images in the params file so that we don't have to
+            # make assumptions about the file rules here
+            my $pathBase = $params->{stack_path_base};
+            $pathBase = $params->{path_base} unless defined $pathBase;
+            unless (defined $pathBase) {
+                my_die("stack path base undefined found when creating exp or expnum image", $job_id, $PS_EXIT_PROG_ERROR);
+            }
+            $pathBase .= ".unconv";
+            my $tmpBase = "$outputBase.TMP";
+            # nocompress because exp image gets corrupted if we do
+            my $command = "$ppstamp $tmpBase $roiArgs  -nocompress -stage stack";
+            # XXX: use file rules
+            # Image is the EXP (exptime) image
+            $command .= " -file $pathBase.exp.fits";
+            # treat number image as mask
+            $command .= " -mask $pathBase.num.fits" if ($options & $PSTAMP_SELECT_NUM);
+
+            $command .= " -forheader $calibfile" if $calibfile;
+
+            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                run(command => $command, verbose => $verbose);
+
+            if (WIFEXITED($error_code)) {
+                $exitStatus = WEXITSTATUS($error_code);
+            } else {
+                print STDERR "ppstamp failed error_code: $error_code\n";
+                $exitStatus = $PS_EXIT_SYS_ERROR;
+            }
+
+            # command to compress fits files to stdout using gzip
+            my $fpack_gzip = "$fpack -g -S";
+            $fpack_gzip .= " -D" unless $save_temps;
+
+            if (!$exitStatus and ($options & $PSTAMP_SELECT_NUM)) {
+                # rename the num image if selected since it is not a mask
+                # Change .exp.mk to .num
+                my $tmpName = "$tmpBase.mk.fits";
+                my $numName = "$outputBase.num.fits";
+                # optionally fpack it while we're here unless user wants uncompressed images
+                unless ($options & $PSTAMP_SELECT_UNCOMPRESSED) {
+                    my $command = "$fpack_gzip $tmpName > $numName";
+                    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                        run(command => $command, verbose => $verbose);
+
+                    if (WIFEXITED($error_code)) {
+                        $exitStatus = WEXITSTATUS($error_code);
+                    } else {
+                        print STDERR "ppstamp failed error_code: $error_code\n";
+                        print STDERR "@$stderr_buf\n";
+                        $exitStatus = $PS_EXIT_SYS_ERROR;
+                    }
+                } elsif (!$exitStatus) {
+                    rename $tmpName, $numName 
+                        or my_die ("failed to rename $tmpName to $numName", $job_id, $PS_EXIT_UNKNOWN_ERROR);
+                }
+
+            }
+            # fpack the exp image if not -nocompress
+            unless ($exitStatus or ($options & $PSTAMP_SELECT_UNCOMPRESSED)) {
+                my $command = "$fpack_gzip $tmpBase.fits > $outputBase.exp.fits";
+                my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                    run(command => $command, verbose => $verbose);
+
+                if (WIFEXITED($error_code)) {
+                    $exitStatus = WEXITSTATUS($error_code);
+                } else {
+                    print STDERR "ppstamp failed error_code: $error_code\n";
+                    print STDERR "@$stderr_buf\n";
+                    $exitStatus = $PS_EXIT_SYS_ERROR;
+                }
+            } elsif (!$exitStatus) {
+                rename "$tmpBase.fits", "$outputBase.exp.fits"
+                    or my_die ("failed to rename $tmpBase.fits to $outputBase.exp.fits", $job_id, $PS_EXIT_UNKNOWN_ERROR);
+            }
+        }
+    } else {
+        $exitStatus = justCopyFiles($outputBase, \$options, $params);
+    }
+
+    if ($exitStatus == 0) {
+        my $reglist = "$outdir/reglist$job_id";
+
+        my $F;
+        open $F, ">$reglist" or my_die( "can't open $reglist for output", $job_id, $PS_EXIT_UNKNOWN_ERROR);
+
+        # Figure out what output images were produced
+
+        # Note: we are assuming the contents of the PSTAMP filerules here.
+        my %extensions = ( $PSTAMP_SELECT_IMAGE    => 'fits', 
+                           $PSTAMP_SELECT_MASK     => 'mk.fits',
+                           $PSTAMP_SELECT_VARIANCE => 'wt.fits',
+                           $PSTAMP_SELECT_SOURCES  => 'cmf',
+                           $PSTAMP_SELECT_JPEG     => 'jpg',
+                           $PSTAMP_SELECT_EXP      => 'exp.fits',
+                           $PSTAMP_SELECT_NUM      => 'num.fits',
+                           $PSTAMP_SELECT_EXPJPEG  => 'exp.jpg',
+                           $PSTAMP_SELECT_NUMJPEG  => 'num.jpg');
+
+        my $output_mask = $options & ($PSTAMP_SELECT_IMAGE | $PSTAMP_SELECT_MASK | $PSTAMP_SELECT_VARIANCE 
+            | $PSTAMP_SELECT_JPEG | $PSTAMP_SELECT_SOURCES
+            | $PSTAMP_SELECT_EXP | $PSTAMP_SELECT_NUM 
+            | $PSTAMP_SELECT_EXPJPEG | $PSTAMP_SELECT_NUMJPEG);
+
+
+        foreach my $key (keys (%extensions)) {
+            my $do_this_one = $key & $output_mask;
+
+            next if (! $do_this_one);
+
+            my $extension = $extensions{$key};
+
+            my $basename = basename($outputBase);
+
+            my $filename = "${basename}.${extension}";
+            my $path  = "${outputBase}.${extension}";
+
+            # XXX is pstamp always the right file type, if not where do we get the right one?
+            print $F file_registration_line($filename, $path, "pstamp") . "\n";
+        }
+
+        get_other_outputs($F, $outputBase, $options, $params);
+
+        close $F;
+        $jobStatus = $PS_EXIT_SUCCESS;
+    } elsif ($exitStatus == $PSTAMP_NO_OVERLAP || $exitStatus == $PSTAMP_NO_VALID_PIXELS) {
+        $jobStatus = $exitStatus;
+    } else {
+        my_die( "ppstamp failed with error code: $exitStatus", $job_id, $exitStatus);
+    }
+} elsif ($jobType eq "get_image") {
+
+    my $pstamp_bundle_root = metadataLookupStr($ipprc->{_siteConfig}, "PSTAMP_BUNDLE_ROOT");
+
+    my $params = read_params_file($outputBase);
+    my $imagedb = $params->{imagedb};
+
+    my $command = "$pstamp_get_image_job --job_id $job_id --output_base $outputBase --rownum $rownum";
+    $command .= " --bundleroot $pstamp_bundle_root" if $pstamp_bundle_root;
+    $command .= " --imagedb $imagedb" if $imagedb;
+    $command .= " --dbname $dbname" if $dbname;
+    $command .= " --dbserver $dbserver" if $dbserver;
+    $command .= " --verbose" if $verbose;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+
+    if ($success) {
+        $jobStatus = $PS_EXIT_SUCCESS;
+    } else {
+        $jobStatus = $error_code >> 8;
+        my_die( "pstamp_get_image_job failed with error code: $jobStatus", $job_id, $jobStatus);
+    }
+} elsif ($jobType eq "detect_query") {
+    # my $outdir = dirname($outputBase);
+    # my $argslist = "$outdir/parse.args";
+    # open ARGSLIST, "<$argslist" or my_die("failed to open argslist file $argslist", $job_id, $PS_EXIT_UNKNOWN_ERROR);
+    # my $argString = <ARGSLIST>;
+    # close ARGSLIST;
+    # chomp $argString;
+
+    # XXX: should we do any other sanity checking?
+    # my_die("arglist file $argslist is empty", $job_id, $PS_EXIT_DATA_ERROR) if !$argString;
+
+    my $command = "$dquery_job_run --job_id $job_id --output_base $outputBase";
+    $command .= " --dbname $dbname" if $dbname;
+    $command .= " --dbserver $dbserver" if $dbserver;
+    $command .= " --verbose" if $verbose;
+    $command .= " --save-temps" if $save_temps;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+	run(command => $command, verbose => $verbose);
+
+    # Although dqueryparse can potentially force more updates (and more jobs), it should still return success.
+    if ($success) {
+	$jobStatus = $PS_EXIT_SUCCESS;
+    } else {
+	$jobStatus = $error_code >> 8;
+	my_die("dquery_job_run.pl failed with error code: $jobStatus", $job_id, $jobStatus);
+    }
+} elsif ($jobType eq "child") {
+    # the only thing jobs of jobType child is to finish
+    $jobStatus = 0;
+} else {
+    my_die("unknown jobType $jobType found", $job_id, $PS_EXIT_PROG_ERROR);
+}
+
+# mark the job stopped in the database
+{
+    my $command = "$pstamptool -updatejob -job_id $job_id -set_state stop"; 
+    $command .= " -set_fault $jobStatus" if $jobStatus;
+    $command .= " -dbname $dbname" if $dbname;
+    $command .= " -dbserver $dbserver" if $dbserver;
+    if (!$no_update) {
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+        unless ($success) {
+            die("Unable to perform $command: $error_code");
+        }
+    } else {
+        print STDERR "skipping command: $command\n"
+    }
+}
+
+exit 0;
+
+# create a string to be passed as input to dsreg when registering this file in a fileset
+# XXX move this to a module so it can be shared
+
+sub file_registration_line {
+    my $filename = shift;
+    my $path     = shift;
+    my $filetype = shift;
+    if (-e $path) {
+        my @finfo = stat($path);
+        my_die("failed to stat $path", $job_id, $PS_EXIT_UNKNOWN_ERROR) unless (@finfo); 
+        my $bytes = $finfo[7];
+        my $md5sum = file_md5_hex($path);
+
+        return "$filename|$bytes|$md5sum|$filetype|";
+    } else {
+        my_die("$filename not found at $path", $job_id, $PS_EXIT_UNKNOWN_ERROR);
+    }
+}
+        
+sub get_other_outputs {
+    my $f = shift;
+    my $output_base = shift;
+    my $options = shift;
+    my $params = shift;
+
+    if ($options & ( $PSTAMP_SELECT_CMF | $PSTAMP_SELECT_PSF | $PSTAMP_SELECT_BACKMDL)) {
+        if (!$params) {
+            $params = read_params_file($output_base);
+        }
+
+        my $stage = $params->{stage};
+
+        # raw files don't have any other data products
+        return 1 if $stage eq "raw";
+
+        # add the other data products if they are selected and exist.
+        # silently skip them if they don't exist. Perhaps this should be
+        # detected in pstampparse so that the user can be notified with 
+        # a message in parse_error.txt ("warp does not have a background model")
+        my $psf_file = $params->{psf} if ($options & $PSTAMP_SELECT_PSF);
+        my $backmdl_file = $params->{backmdl} if ($options & $PSTAMP_SELECT_BACKMDL);
+        my $pattern_file = $params->{pattern} if ($options & $PSTAMP_SELECT_BACKMDL);
+        my $filter = $params->{filter};
+        $filter = ' ' if !$filter;
+	$filter = substr $filter, 0, 1;
+        my $cmf_file;
+#        if ($stage ne 'chip') {
+#            # we don't ship chip stage cmf files because they may not be censored
+#            $cmf_file = $params->{cmf} if ($options & $PSTAMP_SELECT_CMF);
+#        }
+
+        my $outdir = dirname($output_base);
+        my $basename = basename($output_base);
+        my ($rownum, $jobnum, $therest) = split /_/, $basename;
+        &my_die("failed to split basename: $basename", $job_id, $PS_EXIT_CONFIG_ERROR) 
+            if (!$therest or !$rownum or !$jobnum);
+
+        # XXX: Here we are assuming the form of the output file names
+	# if we change this in pstampparse we'll need to remember ....
+	# (the last time I forgot)
+        my $prefix = "${rownum}_${jobnum}_${filter}_";
+
+        if ($cmf_file) {
+            print "cmf file is $cmf_file\n";
+            copy_and_register_file($f, $cmf_file, $outdir, $prefix);
+        }
+        if ($psf_file) {
+            print "psf_file is $psf_file\n";
+            copy_and_register_file($f, $psf_file, $outdir, $prefix);
+        }
+        if ($backmdl_file) {
+            print "backmdl_file is $backmdl_file\n";;
+            copy_and_register_file($f, $backmdl_file, $outdir, $prefix);
+        }
+       if (0) {
+        # don't enable this yet
+        if ($pattern_file) {
+            print "pattern_file is $pattern_file\n";;
+            copy_and_register_file($f, $pattern_file, $outdir, $prefix);
+        }
+       }
+    }
+}
+
+sub read_params_file {
+    my $output_base = shift;
+
+    my $params_file = $output_base . ".mdc";
+    open (IN, "<$params_file") 
+        or my_die("failed to open params file: $params_file", $job_id, $PS_EXIT_UNKNOWN_ERROR);
+
+    my $data = $mdcParser->parse(join "", (<IN>))
+        or my_die("failed to parse params file: $params_file", $job_id, $PS_EXIT_UNKNOWN_ERROR);
+
+    my $components = parse_md_list($data);
+
+    my $n = scalar @$components;
+    if ($n != 1) {
+        my_die("params file $params_file contains unexpected number of components: $n",
+                $job_id, $PS_EXIT_PROG_ERROR);
+    }
+    return $components->[0];
+}
+
+# copy_and_register_file ($f, $src, $destdir, $prefix);
+sub copy_and_register_file {
+    my $F = shift;
+    my $src = shift;
+    my $destdir = shift;
+    my $prefix = shift;
+
+    my $fn = $prefix . basename($src);
+    my $dst = "$destdir/$fn";
+
+    my $resolved = $ipprc->file_resolve($src);
+
+    my_die("failed to resolve $src", $job_id, $PS_EXIT_UNKNOWN_ERROR) if !$resolved;
+
+    if (!-e $resolved) {
+        print STDERR "$src does not exist, skipping\n";
+        return;
+    }
+
+
+    copy($resolved, $dst) or my_die("failed to copy $resolved to $dst", $job_id, $PS_EXIT_UNKNOWN_ERROR);
+
+    print $F file_registration_line($fn, $dst, "fits") . "\n";
+}
+
+sub check_files {
+    my $error_code = shift;
+    my $return_code = 1;
+    foreach my $f (@_) {
+        if (!$ipprc->file_exists($f)) {
+            my $gone;
+            if (storage_object_exists($f, \$gone)) {
+                if ($error_code and $gone) {
+                    my_die( "file $f is GONE:", $job_id, $PSTAMP_GONE, 'stop');
+                } else {
+                    print STDERR "file $f is not available\n";
+                    $return_code = 0;
+                }
+            } else {
+                # This shouldn't happen. The job shouldn't have been queued unless the file
+                # exist. I guess if cleanup got triggered after the job was queued
+                my_die( "file $f does not exist:", $job_id, $error_code, 'stop');
+            }
+        }
+    }
+    return $return_code;
+}
+
+my $neb;
+sub storage_object_exists
+{
+    my $file = shift;
+    my $ref_all_gone = shift;
+
+    if (!$neb) {
+        my $scheme = file_scheme($file);
+        if ($scheme and $scheme eq 'neb') {
+            $neb = $ipprc->nebulous();
+        } else {
+            return 0;
+        }
+    }
+
+    my $exists = $neb->storage_object_exists($file);
+    if (!$exists) {
+        return 0;
+    }
+
+    my $command = "$whichnode $file";
+
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        $error_code = (($error_code >> 8) or $PS_EXIT_PROG_ERROR);
+        &my_die("Unable to perform whichnode: $error_code", $job_id, $PS_EXIT_CONFIG_ERROR, 'run');
+    }
+
+    my @lines = split "\n", (join "", @$stdout_buf);
+
+    if (scalar @lines == 0) {
+        # no output the file is really and truely gone
+        # XXX: this is now caught above
+        print STDERR "storage object for $file does not exist\n";
+        return 0;
+    }
+
+    my $numGone = 0;
+    my $numNotGone = 0;
+    foreach my $line (@lines) {
+        chomp $line;
+
+        # output lines are either
+        #   "volume available"
+        # or 
+        #   "volume not available"
+
+        my ($volume, $answer, undef) = split " ", $line;
+        # our hack is if the volume has an X in the name it's gone
+        if ($volume =~ /X/) {
+            print STDERR "$file is on $volume which is gone\n";
+            $numGone++;
+        } elsif ($answer eq 'available') {
+            $numNotGone++;
+        } elsif ($answer eq 'not') {
+            print STDERR "$file is on $volume which is not available\n";
+            $numNotGone++;
+        } else {
+            print STDERR "unexpected output from whichnode: $line\n";
+        }
+    }
+    # if there are any instances that are not on a gone volume set all_gone to 0
+    if ($numNotGone == 0 and $numGone > 0) {
+        $$ref_all_gone = 1;
+    } else {
+        $$ref_all_gone = 0;
+    }
+
+    # storage object exists so return true
+    return 1;
+}
+
+# stack_summary stage does not currently use proper file rules.
+# The parser defers handlint this to us..
+
+sub update_stack_summary_filenames {
+    my $params  = shift;
+
+    my $path_base = $params->{path_base};
+
+    $params->{image}  = $path_base . ".image.b1.fits";
+    $params->{mask}   = $path_base . ".mask.b1.fits";
+    $params->{weight} = $path_base . ".variance.b1.fits";
+    $params->{jpeg}   = $path_base . ".image.0.b1.jpeg";
+    $params->{exp}    = $path_base . ".exp.b1.fits";
+    $params->{num}    = $path_base . ".num.b1.fits";
+    $params->{expjpeg} = $path_base . ".exp.0.b1.jpeg";
+    $params->{numjpeg} = $path_base . ".num.0.b1.jpeg";
+}
+
+sub myCopy {
+    my ($dest, $src, $type, $dieOnFail) = @_;
+
+    my $result;
+    my $resolved = $ipprc->file_resolve($src);
+    if ($resolved and $ipprc->file_exists($resolved)) {
+        print "Copying $src to $dest\n" if $verbose;;
+        $result = copy($resolved, $dest);
+    } else {
+        my $msg = "Specified source $type image $src not found.";
+        if ($dieOnFail) {
+            $msg .= "\n";
+        } else {
+            $msg .= " ignoring\n";
+        }
+        carp $msg;
+        $result = 0;
+    }
+    
+    if (!$result and $dieOnFail) {
+        &my_die("Unable to copy $type image", $job_id, $PS_EXIT_SYS_ERROR, 'run');
+    }
+    return $result;
+}
+
+sub justCopyFiles {
+    my ($outputBase, $r_options, $params) = @_;
+
+    print "Just copying files for $job_id.\n";
+
+    my $options = $$r_options;
+    if ($options & $PSTAMP_SELECT_IMAGE) {
+        myCopy("$outputBase.fits", $params->{image}, 'image', 1);
+    }
+    if ($options & $PSTAMP_SELECT_MASK) {
+        if (!myCopy("$outputBase.mk.fits", $params->{mask}, 'mask', 0)) {
+            $options &= ~$PSTAMP_SELECT_MASK;
+        }
+    }
+    if ($options & $PSTAMP_SELECT_VARIANCE) {
+        if (!myCopy("$outputBase.wt.fits", $params->{weight}, 'variance', 0)) {
+            $options =  ~$PSTAMP_SELECT_VARIANCE;
+        }
+    }
+    if ($options & $PSTAMP_SELECT_JPEG) {
+        if (!myCopy("$outputBase.jpg", $params->{jpeg}, 'jpeg', 0)) {
+            $options &= ~$PSTAMP_SELECT_JPEG;
+        }
+    }
+    if ($options & $PSTAMP_SELECT_EXP) {
+        if (!myCopy("$outputBase.exp.fits", $params->{exp}, 'exp', 0)) {
+            $options &= ~$PSTAMP_SELECT_EXP;
+        }
+    }
+    if ($options & $PSTAMP_SELECT_NUM) {
+        if (!myCopy("$outputBase.num.fits", $params->{num}, 'num', 0)) {
+            $options &= ~$PSTAMP_SELECT_NUM;
+        }
+    }
+    if ($options & $PSTAMP_SELECT_EXPJPEG) {
+        if (!myCopy("$outputBase.exp.jpg", $params->{expjpeg}, 'exp', 0)) {
+            $options &= ~$PSTAMP_SELECT_EXPJPEG;
+        }
+    }
+    if ($options & $PSTAMP_SELECT_NUMJPEG) {
+        if (!myCopy("$outputBase.num.jpg", $params->{numjpeg}, 'num', 0)) {
+            $options &= ~$PSTAMP_SELECT_NUMJPEG;
+        }
+    }
+
+    $$r_options = $options;
+
+    print "Done with copy.\n";
+
+    return 0;
+}
+
+# use ppBackground or ppBackgroundStack to produce background restored images
+sub create_background_restored_images {
+    my $params = shift;
+    my $stage = $params->{stage};
+
+    if ($stage ne 'stack' and $stage ne 'chip') {
+        # this function is only supported for chip and stack stage
+        # perhaps the parser should catch this ...
+        print STDERR "background restoration not currently supported for stage $params->{stage}\n";
+        return $PSTAMP_BG_RESTORE_NOT_AVAILABLE;
+    }
+
+    my $tempdir = tempdir("$temproot/pstamp.$job_id.XXXX", CLEANUP => !$save_temps);
+    my $imagedb = $params->{imagedb};
+    my $camera = $params->{camera};
+
+    # reinstantiate IPP Configuration using the now known camera so that we get the applicable file rules
+    $ipprc = PS::IPP::Config->new($camera);
+
+    my $input_image = $params->{image};
+    my $input_mask = $params->{mask};
+    my $input_variance = $params->{weight};
+
+    if ($stage eq 'stack') {
+        my $stack_id = $params->{stack_id};
+        my $mdcfile = "$tempdir/stk.$stack_id.bkg_input.mdc";
+        {
+            my $command = "$stack_bkg_mk_mdc --stack_id $stack_id --camera $camera --dbname $imagedb > $mdcfile";
+            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                run(command => $command, verbose => $verbose);
+            unless ($success) {
+                $error_code = (($error_code >> 8) or $PS_EXIT_PROG_ERROR);
+                &my_die("Unable to perform stack_bg_mk_mdc.pl: $error_code", $job_id, $PS_EXIT_CONFIG_ERROR, 'run');
+            }
+        }
+
+        my $outroot = "$tempdir/stk.$stack_id";
+
+        # XXX: get otapath from a config file or detrend system
+        my $otapath = "neb:///detrends/background_OTA_models.20140527/test_solutions";
+        {
+            my $command = "$ppBackgroundStack $outroot -input $mdcfile -image $input_image"
+               . " -OTApath $otapath" ;
+            $command .= " -mask $input_mask" if $input_mask;
+            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                run(command => $command, verbose => $very_verbose);
+            unless ($success) {
+                # we turned off verbosity because ppBackgroundStack emits too much output
+                # XXX: perhaps use a log file and save it with the job data
+                print STDERR join "", @$stderr_buf;
+                $error_code = (($error_code >> 8) or $PS_EXIT_PROG_ERROR);
+                &my_die("Unable to perform ppBackgroundStack: $error_code", $job_id, $PS_EXIT_CONFIG_ERROR, 'run');
+            }
+        }
+        # it's all good change the image to be passed to ppstamp
+        $params->{image} = $ipprc->filename('PPBACKGROUND.STACK.OUTPUT', $outroot) or
+                &my_die("Unable to resolve ppBackgroundStack output file name", $job_id, $PS_EXIT_UNKNOWN_ERROR, 'run');
+
+    } elsif ($stage eq 'chip') {
+        my $input_mdl = $params->{backmdl};
+        my $chip_id = $params->{chip_id};
+        my $class_id = $params->{class_id};
+        my $outroot = "$tempdir/bgrestored.ch.$chip_id";
+        {
+            my $command = "$ppBackground $outroot -background $input_mdl -image $input_image";
+            # XXX: I think ppBackground might always require mask and variance images
+            $command .= " -mask $input_mask";
+            $command .= " -variance $input_variance";
+
+            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                run(command => $command, verbose => $verbose);
+            unless ($success) {
+                print STDERR join "", @$stderr_buf;
+                $error_code = (($error_code >> 8) or $PS_EXIT_PROG_ERROR);
+                &my_die("Unable to perform ppBackground: $error_code", $job_id, $PS_EXIT_CONFIG_ERROR, 'run');
+            }
+        }
+        $params->{image} = $ipprc->filename('PPBACKGROUND.OUTPUT', $outroot, $class_id) or
+                &my_die("Unable to resolve ppBackgroundStack output image name", $job_id, $PS_EXIT_CONFIG_ERROR, 'run');
+        $params->{mask} = $ipprc->filename('PPBACKGROUND.OUTPUT.MASK', $outroot, $class_id) or
+                &my_die("Unable to resolve ppBackgroundStack output mask name", $job_id, $PS_EXIT_CONFIG_ERROR, 'run');
+        $params->{weight} = $ipprc->filename('PPBACKGROUND.OUTPUT.VARIANCE', $outroot, $class_id) or
+                &my_die("Unable to resolve ppBackgroundStack output variance name", $job_id, $PS_EXIT_CONFIG_ERROR, 'run');
+
+    } else {
+        # can't get here
+        &my_die( "background restoration not currently supported for stage $params->{stage}\n",
+            $job_id, $PS_EXIT_PROG_ERROR, 'stop'); ;
+    }
+
+    return 0;
+}
+sub findStaticskyCMF {
+    my $params = shift;
+    my $stack_id = $params->{stack_id};
+
+    &my_die( "NO stack_id found in findStaticskyCMF\n",
+            $job_id, $PS_EXIT_PROG_ERROR, 'run') if !$stack_id;
+
+    my $command = "$staticskytool -dbname $params->{imagedb} -result -stack_id $stack_id";
+    my $results = runToolAndParse($command, $verbose);
+
+    if (!$results) {
+        print "no staticsky results found for stack $stack_id\n";
+        return undef;
+    }
+
+    my $latest;
+    my $latest_sky_id = 0;
+    foreach my $result (@$results) {
+        if ($result->{sky_id} > $latest_sky_id) {
+            $latest = $result;
+            $latest_sky_id = $result->{sky_id};
+        }
+    }
+    if ($latest) {
+        # XXXX Use proper file rule
+        return $latest->{path_base} .  ".stk.$stack_id.cmf";
+    } 
+    # Can't happen can it?
+    return undef;
+}
+
+sub my_die
+{
+    my $msg = shift;            # Warning message on die
+    my $job_id = shift;         # job identifier
+    my $exit_code = shift;      # Exit code to add
+    my $job_state = shift;      # new pstampJob.state
+
+    $exit_code = $PS_EXIT_PROG_ERROR unless $exit_code;
+    if ($exit_code > 100) {
+        carp ("invalid exit code: $exit_code changing to $PS_EXIT_UNKNOWN_ERROR");
+        $exit_code = $PS_EXIT_UNKNOWN_ERROR;
+    }
+    $job_state = 'run' unless $job_state;
+
+    carp($msg);
+    if (defined $job_id and not $no_update) {
+        my $command = "$pstamptool -updatejob";
+        $command .= " -job_id $job_id";
+        $command .= " -set_fault $exit_code";
+        # XXX: fix pstamptool to not require -state when -fault with nonzero value is provided
+        $command .= " -set_state $job_state";
+        $command .= " -dbname $dbname" if defined $dbname;
+        $command .= " -dbserver $dbserver" if defined $dbserver;
+        system($command);
+    }
+    exit $exit_code;
+}
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_listjobs.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_listjobs.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_listjobs.pl	(revision 41103)
@@ -0,0 +1,123 @@
+#!/bin/env perl
+###
+### pstamp_listjobs.pl
+### list the job_id, state, and data store uri for the postage stamp jobs queued for a given 
+### postage stamp request id
+###
+
+use warnings;
+use strict;
+use Getopt::Long qw( GetOptions );
+
+my $verbose;
+my $dbname;
+my $dbserver;
+
+GetOptions(
+    'verbose'   =>  \$verbose,
+    'dbname=s'  =>  \$dbname,
+    'dbserver=s'=>  \$dbserver,
+);
+
+if (@ARGV != 1) {
+    die "usage: $0 request_id\n";
+}
+
+my $request_id = $ARGV[0];
+
+
+use Sys::Hostname;
+my $host = hostname();
+
+## This isn't a script to be launched by pantasks we probably want go get rid of this
+if ($verbose) {
+    print STDERR "\n\n";
+    print STDERR "Starting script $0 on $host\n\n";
+}
+
+use IPC::Cmd 0.36 qw( can_run run );
+
+use PS::IPP::Metadata::Config;
+use PS::IPP::Metadata::Stats;
+use PS::IPP::Metadata::List qw( parse_md_list );
+
+use PS::IPP::Config qw($PS_EXIT_SUCCESS
+		       $PS_EXIT_UNKNOWN_ERROR
+		       $PS_EXIT_SYS_ERROR
+		       $PS_EXIT_CONFIG_ERROR
+		       $PS_EXIT_PROG_ERROR
+		       $PS_EXIT_DATA_ERROR
+		       $PS_EXIT_TIMEOUT_ERROR
+		       metadataLookupStr
+		       metadataLookupBool
+		       caturi
+		       );
+
+my $missing_tools;
+
+my $pstamptool  = can_run('pstamptool')  or (warn "Can't find pstamptool"  and $missing_tools = 1);
+
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files
+
+my @psjobs;
+#Look up the jobs for the given request_id
+{
+    my $command = "$pstamptool -listjob -req_id $request_id";
+    $command .= " -dbname $dbname" if $dbname;
+    $command .= " -dbserver $dbserver" if $dbserver;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        die("Unable to perform pstamptool -pendingreq: $error_code");
+    }
+
+    if (@$stdout_buf == 0) {
+        print STDERR "no pstamp jobs for request $request_id\n" if $verbose;
+        exit 0;
+    }
+    my $metadata = $mdcParser->parse(join "", @$stdout_buf) or
+        die("Unable to parse metdata config doc");
+
+    my $jobs = parse_md_list($metadata);
+
+    @psjobs = @$jobs;
+}
+
+if (! @psjobs) {
+    print STDERR "no postage stamp jobs for request $request_id\n";
+    exit 0;
+}
+
+foreach my $job (@psjobs) {
+    #
+    # convert from filename to data store relative uri
+    # XXX arguably we shouldn't do this here.
+    # This should be a function of the web interface. However, since this script
+    # is only used by the web interface ....
+    my $i = index($job->{outputBase}, "dsroot/");
+
+    my $add_fits = 0;
+    if ($job->{jobType} ne "get_image") {
+#        $add_fits = 1;
+    }
+
+    my $uri;
+    if ($i > 0) {
+        $uri = "/ds" . substr($job->{outputBase}, $i + 6);
+    } else {
+        $uri = "$job->{outputBase}";
+    }
+
+    if ($add_fits) {
+        $uri .= ".fits";
+    }
+
+    print "$job->{job_id} $job->{state} $job->{fault} $job->{name} $job->{outProduct} $uri\n";
+}
+
+exit 0;
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_new_request.sh
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_new_request.sh	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_new_request.sh	(revision 41103)
@@ -0,0 +1,59 @@
+#!/bin/sh
+
+#
+# pstamp_new_request.sh
+#
+# create a postage stamp request file and add it to the data store
+
+# This is a simple program for testing purposes only and not
+# part of the postage stamp server
+
+# TODO: use getopt and take these configuration variables as command
+# line arguments or convert this script to perl and use the ipp config
+
+DATA_STORE=/var/www/html/ds/dsroot
+PRODUCT=pstamprequest
+
+if [[ $# == 0 ]] ; then
+    echo "usage: $0 fileset_id pstamp_request_arguments"
+    exit 22
+fi
+
+fileset_id=$1
+shift
+
+dir_path=${DATA_STORE}/${PRODUCT}/${fileset_id}
+echo $dir_path
+if [[ -e $dir_path ]] ; then
+    echo fileset $fileset_id already exists in $dir_path
+    exit 1
+fi
+
+if ! mkdir -p $dir_path ; then
+    $status = $?
+    echo failed to mkdir $dir_path
+    exit $status
+fi
+
+request_file=${fileset_id}.fits
+
+pstamprequest ${dir_path}/${request_file} -req_name $fileset_id $*
+status=$?
+if [[ $status != 0 ]] ; then
+    echo pstamprequest failed: $status
+    rm -r $dir_path
+    exit $status
+fi
+
+# Invoke the data store registration script
+echo $request_file\|\|\|psrequest\| | dsreg --add $fileset_id --type PSREQUEST --product $PRODUCT  --list -
+
+status=$?
+if [[ $status != 0 ]] ; then
+    echo dsreg failed: $status
+    rm -r $dir_path
+    exit $status
+fi
+
+exit 0
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_parser_run.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_parser_run.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_parser_run.pl	(revision 41103)
@@ -0,0 +1,327 @@
+#!/bin/env perl
+###
+### pstampparser_run.pl
+###     Run the request parser for a given request id
+### This script should be called request_parser.pl since it handles more than postage
+### stamp requests
+###
+
+use warnings;
+use strict;
+
+use Sys::Hostname;
+use Getopt::Long qw( GetOptions );
+use File::Basename qw( basename dirname);
+use File::Copy;
+use POSIX qw( strftime );
+use Carp;
+use IPC::Cmd 0.36 qw( can_run run );
+
+use PS::IPP::Metadata::Config;
+use PS::IPP::Metadata::Stats;
+use PS::IPP::Metadata::List qw( parse_md_list );
+
+use PS::IPP::Config qw( :standard );
+use PS::IPP::PStamp::RequestFile qw( :standard );
+
+my $req_id;
+my $uri;
+my $redirect_output;
+my $product;
+my $outdir;
+my $label;
+my $need_magic;
+my $verbose;
+my $dbname;
+my $dbserver;
+
+GetOptions(
+    'req_id=s'          =>  \$req_id,
+    'uri=s'             =>  \$uri,
+    'product=s'         =>  \$product,
+    'outdir=s'          =>  \$outdir,
+    'label=s'           =>  \$label,
+    'need_magic'        =>  \$need_magic,
+    'redirect-output'   =>  \$redirect_output,
+    'verbose'           =>  \$verbose,
+    'dbname=s'          =>  \$dbname,
+    'dbserver=s'         =>  \$dbserver,
+);
+
+if ($verbose) {
+    my $host = hostname();
+    print "\n\n";
+    print "Starting script $0 on $host\n\n";
+}
+
+$need_magic = 0;
+
+my $missing_tools;
+
+my $pstamptool  = can_run('pstamptool')  or (warn "Can't find pstamptool"  and $missing_tools = 1);
+my $pstampparse = can_run('pstampparse.pl') or (warn "Can't find pstampparse.pl" and $missing_tools = 1);
+my $dqueryparse = can_run('dqueryparse.pl') or (warn "Can't find dqueryparse.pl" and $missing_tools = 1);
+my $dsget = can_run('dsget') or (warn "Can't find dsget" and $missing_tools = 1);
+
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+
+my_die("--req_id --uri --product are required", $req_id, $PS_EXIT_CONFIG_ERROR)
+    if !defined($req_id) or
+       !defined($uri) or
+       !defined($product);
+
+my $ipprc = PS::IPP::Config->new(); # IPP Configuration
+
+my $outputDataStoreRoot = metadataLookupStr($ipprc->{_siteConfig}, 'DATA_STORE_ROOT');
+exit ($PS_EXIT_CONFIG_ERROR) unless defined $outputDataStoreRoot; # lookup failure outputs a message
+my $defaultOutputRoot = $outputDataStoreRoot;
+
+my $pstamp_workdir = metadataLookupStr($ipprc->{_siteConfig}, 'PSTAMP_WORKDIR');
+exit ($PS_EXIT_CONFIG_ERROR) unless defined $pstamp_workdir; # lookup failure outputs a message
+
+if (!$dbserver) {
+    $dbserver =  metadataLookupStr($ipprc->{_siteConfig}, 'PS_DBSERVER');
+}
+
+if (!$outdir or ($outdir eq "NULL")) {
+    # outdir is where all of the files generated for this request are placed
+    # NOTE: this location needs to be kept in sync with the web interface ( request.php )
+    my $datestr = strftime "%Y/%m/%d", gmtime;
+    my $datedir = "$pstamp_workdir/$datestr";
+    if (! -e $datedir ) {
+        my $rc = system "mkdir -p $datedir";
+        if ($rc) {
+            my $status = $rc >> 8;
+            $status = $PS_EXIT_CONFIG_ERROR if !$status;
+            my_die( "failed to create working directory $datedir for request id $req_id", $req_id,
+                $status);
+        }
+    }
+
+    $outdir = "$datedir/$req_id";
+}
+
+if (! -e $outdir ) {
+    mkdir $outdir or my_die("failed to create working directory $outdir for request id $req_id", $req_id,
+        $PS_EXIT_CONFIG_ERROR);
+}
+    
+
+if ($redirect_output) {
+    my $logDest = "$outdir/psparse.$req_id.log";
+    $ipprc->redirect_output($logDest);
+}
+
+my $defaultDSProduct = metadataLookupStr($ipprc->{_siteConfig}, 'PSTAMP_DATA_STORE_PRODUCT');
+exit ($PS_EXIT_CONFIG_ERROR) unless defined $defaultDSProduct;
+    
+my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files
+
+
+my $fn = basename($uri);
+my $new_uri = "$outdir/$fn";
+if ($uri =~ /^http:/) {
+    # if the uri is an http uri download the file 
+    my $command = "$dsget --uri $uri --filename $new_uri --timeout 120";
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        my_die("Unable to perform $command error code: $error_code", $req_id, $error_code >> 8);
+    }
+} elsif ($uri ne $new_uri) {
+    # copy the request file into outdir
+    if (-e $new_uri) {
+        # file exists already delete it incase the previous copy is bogus
+        unlink $new_uri or my_die("failed to unlink $new_uri", $req_id, $PS_EXIT_UNKNOWN_ERROR);
+    }
+    if (! copy $uri, $new_uri) {
+        my_die ("failed to copy request file $uri to workdir $outdir", $req_id, $PS_EXIT_UNKNOWN_ERROR);
+    }
+}
+$uri = $new_uri;
+
+my_die("request file $uri not found", $req_id, $PS_EXIT_UNKNOWN_ERROR) if ! -e $uri;
+
+#  if product was not defined (in database), use the default
+if ($product eq "NULL") {
+    $product = $defaultDSProduct;
+}
+
+my $parse_cmd;
+my $request_type;
+my $reqType;    # for the database
+
+my $request_fault = $PSTAMP_INVALID_REQUEST;
+
+# default action is to process the request after parsing. This can be overridden by
+# PREVIEW mode for pstamp requests
+my $action = 'PROCESS';
+
+if (-r $uri) {
+    # run the appropriate parse command to parse the queue the jobs for this request
+    # first check the extension header to find the EXTNAME
+    $request_type = find_request_type($uri, \$action);
+
+    if ($request_type) {
+        print STDERR "request_type for $req_id is $request_type\n" if $verbose;
+        if ($request_type eq "PS1_PS_REQUEST") {
+            $reqType = 'pstamp';
+            $parse_cmd = "$pstampparse";
+            $parse_cmd .= " --label $label" if $label;
+            $parse_cmd .= " --need_magic" if $need_magic;
+            $request_fault = 0;
+        } elsif ($request_type eq "MOPS_DETECTABILITY_QUERY") {
+            $reqType = 'dquery';
+            $parse_cmd = "$dqueryparse";
+	    $parse_cmd .= " --label $label" if $label;
+            $request_fault = 0;
+        } else {
+            print STDERR "Unknown request type $request_type found in $uri";
+        }
+    } else {
+        print STDERR "No EXTNAME found keyword in $uri";
+    }
+} else {
+    if (-e $uri) {
+        print STDERR "Request file $uri is not readable";
+    } else {
+        print STDERR "Request file $uri does not exist";
+    }
+}
+
+if (!$parse_cmd) {
+    # can't go any farther, set fault and set request state to run 
+    # request_finish.pl will clean up, perhaps notifiying the operator of the input data store
+    # that they sent us a request file that we don't understand
+
+    my $command = "$pstamptool -updatereq -req_id $req_id -set_state run";
+    $command   .= " -set_reqType unknown";
+    $command   .= " -set_fault $request_fault";
+    $command   .= " -dbname $dbname" if $dbname;
+    $command   .= " -dbserver $dbserver" if $dbserver;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        die("Unable to perform $command error code: $error_code");
+    }
+    exit $request_fault;
+}
+
+$parse_cmd .= " --mode queue_job --req_id $req_id --product $product --outdir $outdir --file $uri";
+$parse_cmd .= " --dbname $dbname" if $dbname;
+$parse_cmd .= " --dbserver $dbserver" if $dbserver;
+$parse_cmd .= " --verbose" if $verbose;
+
+my $newState;
+my $fault;
+{
+    my $error_file_name = "$outdir/parse_error.txt";
+    # get rid of any error file from previous attempt to parse this request
+    unlink $error_file_name if (-e $error_file_name);
+
+    # Run the parser
+
+    my $command = "$parse_cmd";
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+
+    # save the contents of stderr (if any) to a file. This is relevant if
+    # the file was parseable but one or more of the rows in the request file generated an error
+    my $errbuf = join "", @$stderr_buf;
+    if ($errbuf) {
+        if (!open OUT, ">$error_file_name") {
+            print STDERR ("unable to open parse_error file $error_file_name");
+        } else {
+            print OUT "$errbuf";
+            close(OUT);
+        }
+        print STDERR $errbuf if $verbose;
+    }
+
+    if ($success) {
+        # XXX: This bit of the postage stamp request API has slipped in here because we need to control
+        # the new state of the request
+        if ($action eq 'PROCESS') {
+            $newState = 'run';
+        } elsif ($action eq 'PREVIEW') {
+            $newState = 'parsed';
+        } else {
+            print STDERR "WARNING Ignoring unexpected value for ACTION found in request header: $action\n";
+        }
+    } else {
+        $fault = $error_code >> 8;
+    }
+}
+
+#
+# update the state of this request from 'new' to 'run' and set the uri to the downloaded location
+#
+{
+    my $command = "$pstamptool -updatereq -req_id $req_id";
+    $command   .= " -set_state $newState" if $newState;
+    $command   .= " -set_outdir $outdir";
+    $command   .= " -set_reqType $reqType" if $reqType;
+    $command   .= " -set_uri $new_uri" if $new_uri;
+    $command   .= " -set_fault $fault" if $fault;
+    $command   .= " -dbname $dbname" if $dbname;
+    $command   .= " -dbserver $dbserver" if $dbserver;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        die("Unable to perform $command error code: $error_code");
+    }
+}
+
+# Note: We do not return $fault here. If there was a fatal error we've already exited.
+# If we got a fault it's due to bad input from the user we've set things up for this to be
+# handled by the task pstamp.request.finish
+
+exit 0;
+
+
+sub find_request_type {
+    # find the EXTNAME in the input fits table
+    my $file_name = shift;
+    my $r_action = shift;
+
+    my $out = `echo $file_name | fields -x 0 EXTNAME ACTION`;
+
+    if ($out) {
+        # output from fields is filename value
+        my ($dummy, $extname, $action) = split " ", $out;
+
+        # Set the action if it is defined in the request header
+        # XXX:consider doing this only if extname is PS1_PS_REQUSET
+        $$r_action = $action if ($action);
+
+        return $extname;
+    } else {
+        return undef;
+    }
+}
+
+sub my_die {
+    my $msg = shift;
+    my $req_id = shift;
+    my $fault = shift;
+
+    carp($msg);
+
+    if (!$req_id) {
+        exit $PS_EXIT_CONFIG_ERROR;
+    }
+
+    my $command = "$pstamptool -updatereq -req_id $req_id  -set_fault $fault";
+    $command   .= " -dbname $dbname" if $dbname;
+    $command   .= " -dbserver $dbserver" if $dbserver;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        die("Unable to perform $command error code: $error_code");
+    }
+    exit $fault;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_queue_cleanup.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_queue_cleanup.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_queue_cleanup.pl	(revision 41103)
@@ -0,0 +1,80 @@
+#!/bin/env perl
+#
+# run pstamptool to queue requests older than preserve-days to be cleaned
+
+use strict;
+use warnings;
+
+use PS::IPP::Config 1.01 qw( :standard );
+use IPC::Cmd 0.36 qw( can_run run );
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+
+my $verbose;
+my $save_temps;
+my $preserve_days;
+my $dbname;
+my $dbserver;
+
+GetOptions(
+    'preserve-days=s'       => \$preserve_days,     # clean up requests that stopped this many days ago
+    'verbose'               => \$verbose,
+    'save-temps'            => \$save_temps,
+    'dbserver=s'            => \$dbserver,
+    'dbname=s'              => \$dbname,
+) or pod2usage( 2 );
+
+my $missing_tools;
+my $pstamptool   = can_run('pstamptool') 
+    or (warn "Can't find pstamptool" and $missing_tools = 1);
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit($PS_EXIT_CONFIG_ERROR);
+}
+
+my $ipprc = PS::IPP::Config->new(); # IPP Configuration
+
+if (!$dbserver) {
+    $dbserver =  metadataLookupStr($ipprc->{_siteConfig}, 'PS_DBSERVER');
+    $pstamptool .= " -dbserver $dbserver" if $dbserver;
+}
+
+if (!$dbname) {
+    $dbname =  metadataLookupStr($ipprc->{_siteConfig}, 'PS_DBNAME');
+    $pstamptool .= " -dbname $dbname" if $dbname;
+}
+
+
+
+if (!$preserve_days) {
+    $preserve_days = metadataLookupStr($ipprc->{_siteConfig}, 'PSTAMP_PRESERVE_DAYS');
+    if (!$preserve_days) {
+        $preserve_days = 14;
+    }
+}
+
+if ($preserve_days < 0) {
+    warn("preserve-days cannot be negative\n" );
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+my $ticks = time();
+$ticks -= 86400 * $preserve_days;
+
+my ($sec, $min, $hour, $mday, $month, $year) = gmtime($ticks);
+$year += 1900;
+$month += 1;
+
+my $timestamp_end = sprintf "%4d-%02d-%02dT%02d:%02d:%02d", $year, $month, $mday, $hour, $min, $sec;
+
+my $command = "$pstamptool -updatereq -set_state goto_cleaned -state stop -timestamp_end $timestamp_end";
+my  ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+    run(command => $command, verbose => $verbose);
+unless ($success) {
+    $error_code = (($error_code >> 8) or 1);
+    warn("$command failed. exit status: $error_code");
+    exit $error_code;
+}
+
+
+exit 0;
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_queue_requests.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_queue_requests.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_queue_requests.pl	(revision 41103)
@@ -0,0 +1,239 @@
+#!/bin/env perl
+#
+# pstamp_queue_reqests.pl
+#
+# Query registered data stores for new postage stamp requests and add them to
+# the database of pending requests.
+#
+
+use warnings;
+use strict;
+
+use Getopt::Long qw( GetOptions );
+
+use Sys::Hostname;
+my $host = hostname();
+
+my $verbose;
+my $dbname;
+my $dbserver;
+my $limit;
+my $timeout = 10;
+
+GetOptions(
+    'timeout=i'     =>  \$timeout,
+    'verbose'       =>  \$verbose,
+    'dbname=s'      =>  \$dbname,
+    'dbserver=s'    =>  \$dbserver,
+    'limit=i'       =>  \$limit,
+);
+
+if ($verbose) {
+    print STDERR "\n\n";
+    print STDERR "Starting script $0 on $host\n\n";
+}
+
+use IPC::Cmd 0.36 qw( can_run run );
+
+use PS::IPP::Metadata::Config;
+use PS::IPP::Metadata::Stats;
+use PS::IPP::Metadata::List qw( parse_md_list );
+
+use PS::IPP::Config qw($PS_EXIT_SUCCESS
+		       $PS_EXIT_UNKNOWN_ERROR
+		       $PS_EXIT_SYS_ERROR
+		       $PS_EXIT_CONFIG_ERROR
+		       $PS_EXIT_PROG_ERROR
+		       $PS_EXIT_DATA_ERROR
+		       $PS_EXIT_TIMEOUT_ERROR
+		       metadataLookupStr
+		       metadataLookupBool
+		       caturi
+		       );
+
+my $missing_tools;
+
+my $pstamptool  = can_run('pstamptool')  or (warn "Can't find pstamptool"  and $missing_tools = 1);
+my $dsproductls = can_run('dsproductls') or (warn "Can't find dsproductls" and $missing_tools = 1);
+my $dsfilesetls = can_run('dsfilesetls') or (warn "Can't find dsfilesetls" and $missing_tools = 1);
+
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+my $ipprc = PS::IPP::Config->new();
+my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files
+
+if (!$dbserver) {
+    $dbserver =  metadataLookupStr($ipprc->{_siteConfig}, 'PS_DBSERVER');
+}
+
+my @dataStores;
+#Look up the list of data stores that are ready to be queried
+{
+    my $command = "$pstamptool -datastore -ready";
+    $command .= " -dbname $dbname" if $dbname;
+    $command .= " -dbserver $dbserver" if $dbserver;
+
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        my $rc = $error_code >> 8;
+        die("Unable to perform pstamptool -datastore: $rc");
+    }
+
+    if (@$stdout_buf == 0) {
+        print STDERR "no data stores ready\n" if $verbose;
+        exit 0;
+    }
+    my $metadata = $mdcParser->parse(join "", @$stdout_buf) or
+        die("Unable to parse metdata config doc");
+
+    my $ds = parse_md_list($metadata);
+
+    @dataStores = @$ds;
+}
+
+if (! @dataStores) {
+    print STDERR "no postage stamp data stores found\n" if $verbose;
+    exit 1;
+}
+
+# loop over the data stores
+foreach my $ds (@dataStores) {
+    my $lastFileset;
+
+    # skip any data stores that aren't enabled
+
+    if ($ds->{state} ne "enabled") {
+        next;
+    }
+
+    my $outProduct = $ds->{outProduct};
+    my $ds_id = $ds->{ds_id};
+    my $ds_label = $ds->{label};
+    $ds_label = undef if $ds_label eq "NULL";
+    my @lines;
+    {
+        my $command = "$dsproductls --uri $ds->{uri}/index.txt";
+        $command .= " --last_fileset $ds->{lastFileset}" if $ds->{lastFileset};
+        $command .= " --timeout $timeout";
+
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+        unless ($success) {
+            # dsproductls exit status is the http error code - 300
+            my $exit_status = $error_code >> 8;
+
+            # don't die on "common faults"
+            # perhaps after some number of these we should mark the data store as disabled
+            if (($exit_status == 200) or ($exit_status == 104)) {
+                ## now update the last_fileset column in pstampDataStore
+                update_ds_timestamp($ds_id, $dbname, $dbserver);
+                next;
+            }
+
+
+            die("Unable to perform $command: $error_code");
+        }
+
+        if (@$stdout_buf == 0) {
+            print STDERR "no new request files in data store $ds_id\n" if $verbose;
+            update_ds_timestamp($ds_id, $dbname, $dbserver);
+            next; # next data store
+        }
+        my $out_buf = join("", @$stdout_buf);
+        # split raw output into lines
+        @lines = split /^/, $out_buf;
+    }
+
+    #
+    # each line contains a fileset
+    #
+
+    # number that we've processed
+    my $numFilesets = 0;
+    foreach my $line (@lines) {
+        # parse the line into fields split by whitespace
+        my ($uri, $fs_name, $date, $type) = split " ", $line;
+
+        # skip comment lines
+        next if ( $uri =~ /^#.*/);
+
+        $numFilesets++;
+
+        my @files;
+        {
+            my $command = "$dsfilesetls --uri $uri";
+            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                    run(command => $command, verbose => $verbose);
+            unless ($success) {
+                # we don't want to die here. We need to set lastFileset
+                # so that we don't re queue requsts that we have proccessed
+                die("Unable to perform $command: $error_code");
+            }
+
+            if (@$stdout_buf == 0) {
+                print STDERR "no file sets found\n";
+                next;
+            }
+            my $out_buf = join "", @$stdout_buf;
+            @files = split /^/, $out_buf;
+        }
+
+        #
+        # For each file in the fileset add a request
+        #
+        foreach my $file (@files) {
+            my ($req_uri, $fn, $size, $md5sum, $type, $chipname) = split " ", $file;
+
+            # skip comment lines
+            next if $req_uri =~ (/^#.*/);
+            {
+                my $command = "$pstamptool -addreq -uri $req_uri -ds_id $ds_id";
+                $command .= " -label $ds_label" if $ds_label;
+                $command .= " -dbname $dbname" if $dbname;
+                $command .= " -dbserver $dbserver" if $dbserver;
+
+                my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                        run(command => $command, verbose => $verbose);
+
+                unless ($success) {
+                    die("Unable to perform $command: $error_code");
+                }
+            }
+        }
+        $lastFileset = $fs_name;
+
+        {
+        ## now update the last_fileset column in pstampDataStore
+        my $command = "$pstamptool -ds_id $ds_id -moddatastore -set_last_fileset $lastFileset";
+        $command .= " -dbname $dbname" if $dbname;
+        $command .= " -dbserver $dbserver" if $dbserver;
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+            unless ($success) {
+                die("Unable to perform pstamptool -moddatastore: $error_code");
+            }
+        }
+        last if ($numFilesets >= $limit);
+    }
+}
+
+exit 0;
+
+sub update_ds_timestamp {
+    my $ds_id = shift;
+    my $dbname = shift;
+    my $dbserver = shift;
+
+    my $command = "$pstamptool -ds_id $ds_id -moddatastore -update_timestamp";
+                $command .= " -dbname $dbname" if $dbname;
+                $command .= " -dbserver $dbserver" if $dbserver;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        die("Unable to perform pstamptool -moddatastore: $error_code");
+    }
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_queue_update_cleanup.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_queue_update_cleanup.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_queue_update_cleanup.pl	(revision 41103)
@@ -0,0 +1,98 @@
+#!/bin/env perl
+#
+# run pstamptool to queue requests older than preserve-days to be cleaned
+
+use strict;
+use warnings;
+
+use PS::IPP::Config 1.01 qw( :standard );
+use IPC::Cmd 0.36 qw( can_run run );
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+
+my $verbose;
+my $very_verbose = 0;
+my $save_temps;
+my $preserve_days;
+my $dbname;
+my $dbserver;
+my $imagedb;
+my $label;
+
+GetOptions(
+    'preserve-days=s'       => \$preserve_days,     # clean up requests that stopped this many days ago
+    'verbose'               => \$verbose,
+    'save-temps'            => \$save_temps,
+    'imagedb=s'             => \$imagedb,
+    'label=s'               => \$label,
+    'dbserver=s'            => \$dbserver,
+    'dbname=s'              => \$dbname,
+) or pod2usage( 2 );
+
+my $missing_tools;
+my $pstamptool   = can_run('pstamptool') 
+    or (warn "Can't find pstamptool" and $missing_tools = 1);
+my $chiptool   = can_run('chiptool') 
+    or (warn "Can't find chiptool" and $missing_tools = 1);
+my $warptool   = can_run('warptool') 
+    or (warn "Can't find warptool" and $missing_tools = 1);
+my $difftool   = can_run('difftool') 
+    or (warn "Can't find difftool" and $missing_tools = 1);
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit($PS_EXIT_CONFIG_ERROR);
+}
+
+if (!$imagedb)  {
+    $imagedb = 'gpc1';
+}
+
+if (!$label)  {
+    $label = 'ps_ud%';
+}
+
+my $ipprc = PS::IPP::Config->new(); # IPP Configuration
+
+if (!$dbserver) {
+    $dbserver =  metadataLookupStr($ipprc->{_siteConfig}, 'PS_DBSERVER');
+}
+$pstamptool .= " -dbserver $dbserver" if $dbserver;
+
+if (!$dbname) {
+    $dbname =  metadataLookupStr($ipprc->{_siteConfig}, 'PS_DBNAME');
+}
+$pstamptool .= " -dbname $dbname" if $dbname;
+
+
+my $command = "$pstamptool -pendingdependent -includefaulted -simple";
+my  ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+    run(command => $command, verbose => $very_verbose);
+unless ($success) {
+    $error_code = (($error_code >> 8) or 1);
+    warn("$command failed. exit status: $error_code");
+    exit $error_code;
+}
+
+my @lines = split "\n", (join "", @$stdout_buf);
+my $nlines = scalar @lines;
+
+if ($nlines) {
+    # printing to stderr so that it shows up in the task's log file. stdout is set to null
+    print STDERR "Skipping cleanup of update labels because there are currently $nlines dependents outstanding.\n";
+} else  {
+    print STDERR "Queuing cleanup of update labels because there are no dependents outstanding.\n";
+    foreach my $tool ($chiptool, $warptool, $difftool) {
+        my $command = "$tool -updaterun -set_state goto_cleaned -set_label goto_cleaned -label $label";
+        $command .= " -dbname $imagedb";
+
+        my  ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $very_verbose);
+        unless ($success) {
+            $error_code = (($error_code >> 8) or 1);
+            warn("$command failed. exit status: $error_code");
+            exit $error_code;
+        }
+    }
+}
+
+exit 0;
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_request_file
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_request_file	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_request_file	(revision 41103)
@@ -0,0 +1,477 @@
+#!/usr/bin/env perl
+
+# create a Postage Stamp Request file from a textual description
+
+use warnings;
+use strict;
+
+use Astro::FITS::CFITSIO qw( :constants );
+Astro::FITS::CFITSIO::PerlyUnpacking(1);
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+use File::Basename;
+
+use constant EXTNAME => 'PS1_PS_REQUEST'; # Extension name for output table
+
+my ( $input,			# Name of input text file
+     $output,			# Name of output table
+     $req_name, 
+     $email, 
+     $help
+     );
+
+GetOptions(
+	   'input|i=s'    => \$input,
+	   'output|o=s'   => \$output,
+	   'req_name|r=s' => \$req_name,
+	   'email=s'      => \$email,
+           'help|h'       => \$help,
+) or pod2usage( 2 );
+
+printhelp($0) if $help;
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+
+unless (defined $input) {
+    print STDERR "Required options: --input\n";
+    printhelp($0);
+}
+
+# The header kewords
+my $header = [
+        { name =>  'REQ_NAME', 
+                    writetype => TSTRING, 
+                    comment => 'Postage Stamp request name',
+                    value => undef
+        },
+        { name =>  'EXTVER', 
+                    writetype => TSTRING, 
+                    comment => 'Extension version',
+                    value => undef
+        },
+        { name => 'ACTION',
+                    writetype => TSTRING,
+                    comment => 'Action to perform: LIST or PROCESS',
+                    value => undef
+        },
+#        { name => 'USERNAME',
+#                    writetype => TSTRING,
+#                    comment => 'username for request (optional)',
+#                    value => undef
+#        },
+        { name => 'EMAIL',
+                    writetype => TSTRING,
+                    comment => 'email for submittor (optional)',
+                    value => undef
+        },
+];
+
+my $email_column_num = 3;
+
+# Specification of columns to write
+my $columns = [ 
+        { name => 'ROWNUM',     type => 'J',   writetype => TULONG }, 
+
+        { name => 'CENTER_X',   type => 'D',  writetype => TDOUBLE },
+        { name => 'CENTER_Y',   type => 'D',  writetype => TDOUBLE },
+        { name => 'WIDTH',      type => 'D',  writetype => TDOUBLE },
+        { name => 'HEIGHT',     type => 'D',  writetype => TDOUBLE },
+        # 2 bits in COORD_MASK indicate what units of roi coords are
+        { name => 'COORD_MASK', type => 'J',  writetype => TULONG },
+
+        { name => 'JOB_TYPE',   type => '16A', writetype => TSTRING },
+        { name => 'OPTION_MASK',type => 'J',   writetype => TULONG },
+
+        # image selection parameters
+        { name => 'PROJECT',    type => '16A', writetype => TSTRING },
+        # new in version 2
+        { name => 'SURVEY_NAME',type => '16A', writetype => TSTRING },
+        { name => 'IPP_RELEASE', type => '64A', writetype => TSTRING },
+        # end of new in version 2
+        { name => 'REQ_TYPE',   type => '16A', writetype => TSTRING },
+        { name => 'IMG_TYPE',   type => '16A', writetype => TSTRING },
+        { name => 'ID',         type => '16A', writetype => TSTRING },           
+        { name => 'TESS_ID',    type => '64A', writetype => TSTRING },
+        { name => 'COMPONENT',  type => '64A', writetype => TSTRING },
+
+        { name => 'DATA_GROUP ',type => '64A', writetype => TSTRING },
+
+        { name => 'REQFILT',    type => '16A', writetype => TSTRING },
+        { name => 'MJD_MIN',    type => 'D',   writetype => TDOUBLE },
+        { name => 'MJD_MAX',    type => 'D',   writetype => TDOUBLE },
+        # new in version 2
+        { name => 'RUN_TYPE',   type => '16A', writetype => TSTRING },
+        { name => 'FWHM_MIN',   type => 'D',   writetype => TDOUBLE },
+        { name => 'FWHM_MAX',   type => 'D',   writetype => TDOUBLE },
+
+        { name => 'COMMENT ',   type => '64A', writetype => TSTRING },
+];
+my $columns_v1 = [ 
+        { name => 'ROWNUM',     type => 'J',   writetype => TULONG }, 
+
+        { name => 'CENTER_X',   type => 'D',  writetype => TDOUBLE },
+        { name => 'CENTER_Y',   type => 'D',  writetype => TDOUBLE },
+        { name => 'WIDTH',      type => 'D',  writetype => TDOUBLE },
+        { name => 'HEIGHT',     type => 'D',  writetype => TDOUBLE },
+        # 2 bits in COORD_MASK indicate what units of roi coords are
+        { name => 'COORD_MASK', type => 'J',  writetype => TULONG },
+
+        { name => 'JOB_TYPE',   type => '16A', writetype => TSTRING },
+        { name => 'OPTION_MASK',type => 'J',   writetype => TULONG },
+
+        # image selection parameters
+        { name => 'PROJECT',    type => '16A', writetype => TSTRING },
+        { name => 'REQ_TYPE',   type => '16A', writetype => TSTRING },
+        { name => 'IMG_TYPE',   type => '16A', writetype => TSTRING },
+        { name => 'ID',         type => '16A', writetype => TSTRING },           
+        { name => 'TESS_ID',    type => '64A', writetype => TSTRING },
+        { name => 'COMPONENT',  type => '64A', writetype => TSTRING },
+
+        { name => 'DATA_GROUP ',type => '64A', writetype => TSTRING },
+
+        { name => 'REQFILT',    type => '16A', writetype => TSTRING },
+        { name => 'MJD_MIN',    type => 'D',   writetype => TDOUBLE },
+        { name => 'MJD_MAX',    type => 'D',   writetype => TDOUBLE },
+
+        { name => 'COMMENT ',   type => '64A', writetype => TSTRING },
+];
+
+my $in;
+if ($input eq '-') {
+    $in = \*STDIN;
+} else {
+    open $in, "<$input" or die "cannot open $input for reading";
+}
+
+# build array of arrays for the column data. Note if the input file is version 1 then
+# some of thes will get dropped
+my @colData;
+foreach (@$columns) {
+    push @colData, [];
+}
+
+
+my $minimum_cols = 6;
+my $extver = 2;
+my $numRows = read_data_for_table($in,'\s+', \@colData, $header, $minimum_cols, \$extver); 
+if (!$numRows) {
+    print STDERR "no data in $input\n";
+    exit 1;
+}
+
+# overwrite the REQ_NAME value from the input file with the command
+# line argument
+
+if ($req_name) {
+    $header->[0]->{value} = $req_name;
+} else {
+    $req_name = $header->[0]->{value};
+}
+if ($email) {
+    $header->[$email_column_num]->{value} = $email;
+}
+
+
+die "no request name defined" unless defined $req_name;
+
+$output = $req_name . ".fits" if !$output;
+
+my $status = make_fits_table($output, EXTNAME, $numRows, \@colData, $extver > 1 ? $columns : $columns_v1, $header);
+
+exit $status;
+
+# XXXXX: This should be in a module
+# two utility functions that may be used to create a FITS binary
+# table from hashes describing the header keywords and columns
+
+# read_table_description reads the data for a table from a simple text file
+# make_fits_table writes out the table to a named file
+
+
+# A function to build a fits binary table from supplied data 
+# 
+sub make_fits_table {
+        my $output = shift;     # name of output file
+        my $extname = shift;    # extension name
+        my $numRows = shift;    # number of rows in the table
+        my $colData = shift;    # ref to array of arrays containing the data for each column
+        my $columns = shift;    # ref to array of column descriptions (each a hash)
+                                # with keys: name, type, and writetype
+        my $header = shift;     # ref to array of header keyword descriptions - each a hash
+                                # with keys: name, name, writetype, comment, and value
+        my $status = 0;
+
+        die "incorrect arguments" if !defined($columns);
+        # note $header can be nil
+
+        # build arrays for cfitsio
+        my @colNames;			# Names of columns
+        my @colTypes;			# Types of columns
+        my @colWriteType;               # type to use to write
+
+        foreach my $colSpec ( @$columns) {
+            push @colNames, $colSpec->{name};
+            push @colTypes, $colSpec->{type};
+            push @colWriteType, $colSpec->{writetype};
+        }
+
+        if (-e $output) {
+            unlink "$output" or die "failed to remove existing $output";
+        }
+
+        my $outFits = Astro::FITS::CFITSIO::create_file( $output, $status ); # Output file handle
+        check_fitsio( $status );
+
+        $outFits->create_img( 16, 0, undef, $status );
+        check_fitsio( $status );
+
+        # Create the table
+
+        $outFits->create_tbl( BINARY_TBL(), $numRows, scalar @colNames,
+                                \@colNames, \@colTypes, undef, $extname, $status );
+        check_fitsio( $status );
+
+        # if header keyword descriptions were provided add them
+        if ($header) {
+            foreach my $headerword ( @$header ) {
+                my $value = $headerword->{value};
+                unless (defined $value) {
+                    print "Can't find header keyword $headerword\n";
+                    next;
+                }
+                # zap quotation marks
+                $value =~ s/\'//g;
+                my $name    = $headerword->{name};
+                my $type    = $headerword->{writetype};
+                my $comment = $headerword->{comment};
+                $outFits->write_key( $type, $name, $value, $comment, $status );
+                check_fitsio( $status );
+            }
+        }
+
+
+        for (my $i = 0; $i < scalar @colNames; $i++) {
+            my $writeType = $colWriteType[$i];
+            my $data = $colData->[$i];
+            if ($writeType == TULONG) {
+                die "invalid integer data found in column $i\n" unless validIntegers($data);
+            } elsif ($writeType == TDOUBLE) {
+                die "invalid numeric data found in column $i\n" unless validNumbers($data);
+            }
+            $outFits->write_col( $writeType, $i + 1, 1, 1, $numRows, $data, $status );
+            check_fitsio( $status );
+        }
+
+        $outFits->close_file( $status );
+
+        return 0;
+
+} # end of sub make_fits_table
+
+
+
+# read the table contents from a file
+#
+# input text file format:
+#   lines that begin with '#' are comment lines and are skipped.
+#   other lines are data. Each data line is split into fields with the
+#   provided separator
+#
+# if $header is not null header the first non-commented line is read to
+# fill the value for each header keyword. The number of fields must match
+# the number of keywords.
+#
+# Following the optional header data, each data line contains data for each
+# row in the table. The number of fields must match the number of column
+# arrays provided.
+
+sub read_data_for_table {
+    my $in      = shift;    # input file handle
+    my $sep     = shift;    # string containing field separator
+    my $colData = shift;    # reference to an array of arrays for the data
+    my $header  = shift;    # rerence to array of header keyword descriptions
+    my $minimum_required_vals = shift;
+    my $r_extver = shift;   # reference to extension version
+
+    my $line_num = 0;
+
+    # read data for header if any data is expected
+    if ($header) {
+        my $nhead = @$header;
+        while (my $line = <$in>) {
+            $line_num++;
+            chomp $line;
+            next if !$line;             # skip blank lines
+            next if ($line =~ /^#/);    # skip comment lines
+            my @vals = split /$sep/, $line;
+            my $nvals = @vals;
+            die "two few columns $nvals in header definition. Require at least 2" if @vals < 2;
+
+            $$r_extver = $vals[1];
+            if ($$r_extver > 1) {
+                die "number of header columns in input $nvals does not equal expected number of header words $nhead"
+                    if (@vals != @$header);
+            } else {
+                # drop header keywords not used in version 1
+                for (my $i = @$header - 1; $i >= $nvals; $i--) {
+                    pop @$header;
+                }
+            }
+
+            for (my $i=0; $i < $nvals; $i++) {
+                my $val = $vals[$i];
+                if (defined $val) {
+                    $header->[$i]->{value} = $val;
+                } else {
+                    $header->[$i]->{value} = 'null';
+                }
+            }
+
+            last; # one line defines the header
+        }
+    }
+    if ($$r_extver == 1) {
+        # pop off the colData arrays to account for the unused columns
+        pop @colData;
+        pop @colData;
+        pop @colData;
+        pop @colData;
+        pop @colData;
+    }
+
+    my $num_rows = 0;
+    my $ncols = @$colData - 1;  # -1 because COMMENT is handled seperatly
+    my @last_vals;
+    my %used_ROWNUMS;
+    my $auto_row_num = 0;
+    while (my $line = <$in>) {
+        chomp $line;
+        $line_num++;
+        next if !$line;             # skip blank lines
+        next if ($line =~ /^#/);    # skip comment lines
+
+        my ($spec, $comment) = split /\|/, $line;
+        if (!$spec) {
+            print STDERR "improper format on line $line_num\n";
+            print STDERR "$line\n";
+            exit 1;
+        }
+        # trim leading whitespace from comment
+        $comment =~ s/^\s+// if $comment;
+        if ($comment) {
+            if ($comment =~ /#/) {
+                print STDERR "WARNING '#' character removed from comment on line $line_num.\n";
+                $comment =~ s/#/ /;
+            }
+            if (length($comment) > 63) {
+                print STDERR "WARNING comment too long truncating to 63 characters on line $line_num.\n";
+                $comment = substr($comment, 0, 63);
+            }
+        } else {
+            $comment = "null";
+        }
+        my @vals = split /$sep/, $spec;
+        my $nvals = @vals;
+        if ($nvals < $minimum_required_vals) {
+            die "Too few values $nvals found at line $line_num. Each row must contain at least $minimum_required_vals columns\n";
+        }
+        if ($nvals < $ncols) {
+            if (!scalar @last_vals) {
+                die "Too few values $nvals found at line $line_num. $ncols values are required\n";
+            }
+            for (my $i = $nvals; $i < $ncols; $i++) {
+                $vals[$i] = $last_vals[$i];
+            }
+        }
+        $vals[$ncols] = $comment;
+        @last_vals = @vals;
+    
+        # check the input ROWNUM value. If zero set it automatically
+        my $this_ROWNUM = $vals[0];
+        $this_ROWNUM = ++$auto_row_num if $this_ROWNUM eq 0;
+
+        # fail if this ROWNUM value has already been used
+        my $previous = $used_ROWNUMS{$this_ROWNUM};
+        if ($previous) {
+            die "ROWNUM for line $line_num: $this_ROWNUM has already been used at line $previous\n";
+        }
+        $used_ROWNUMS{$this_ROWNUM} = $line_num;
+
+        $colData->[0]->[$num_rows] = $this_ROWNUM;
+        for (my $col = 1; $col < @$colData; $col++) {
+            $colData->[$col]->[$num_rows] = $vals[$col];
+        }
+        $num_rows++;
+    }
+
+    # we return the number of rows read
+    return $num_rows;
+}
+
+# From Astro::FITS::CFITSIO demo
+sub check_fitsio
+{
+    my $status = shift;		# Status of FITSIO calls
+
+    if ($status != 0) {
+	my $msg;		# Message to output
+	Astro::FITS::CFITSIO::fits_get_errstatus( $status , $msg );
+	die "CFITSIO error: $msg\n";
+    }
+}
+
+sub checkValid {
+    my $aref = shift;
+    my $float = shift;
+
+    return 0 if !defined $aref;
+
+    my $valid = 0;
+    my $row = 0;
+    foreach my $val (@$aref) {
+        $row++;
+        if ($float) {
+            if (!($val =~  /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/)) {
+                $valid = 0;
+                print STDERR "Error on row $row: '$val' is not a valid number\n";
+                last;
+            }
+        } else {
+            if ($val =~ /\D/) {
+                $valid = 0;
+                print STDERR "Error on row $row: '$val' is not a valid integer\n";
+                last;
+            }
+        }
+        $valid = 1;
+    }
+
+    return $valid;
+}
+sub validIntegers {
+    return checkValid(@_, 0);
+}
+
+sub validNumbers
+{
+    return checkValid(@_, 1);
+}
+
+
+sub printhelp
+{
+    my $prog = basename($_[0]);
+
+    print "Create a postage stamp request fits file from a textual description.\n";
+    print "Usage:\n";
+    print "\t$prog --input input_file_name [--req_name request_name] [--output output_file_name ]\n\n";
+    print "If --req_name is provided the REQ_NAME value in the input file is ignored.\n";
+    print "If --output is omitted the output file name is set to REQ_NAME.fits\n\n";
+    print "Header 1 Line.  Format:\n\n";
+    print "REQ_NAME EXTVER ACTION USERNAME EMAIL\n\n";
+    print "REQUEST specification (1 or more lines). Format:\n\n";
+    print "ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE | COMMENT\n\n";
+
+    exit 0;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_results_file.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_results_file.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_results_file.pl	(revision 41103)
@@ -0,0 +1,295 @@
+#!/bin/env perl
+
+# program to create a fits binary table with EXTNAME PS1_PS_RESULTS
+
+use warnings;
+use strict;
+
+use Astro::FITS::CFITSIO qw( :constants );
+Astro::FITS::CFITSIO::PerlyUnpacking(1);
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+use Math::Trig;
+use Data::Dumper;
+
+use constant EXTNAME => 'PS1_PS_RESULTS'; # Extension name for output table
+use constant EXTVER =>  1;
+
+my ( $input,			# Name of input Detectabilty Query table
+     $output,			# Name of output table
+     $save_temps,		# Save temporary files?
+     );
+
+GetOptions(
+	   'input|i=s'    => \$input,
+	   'output|o=s'   => \$output,
+	   'save-temps'   => \$save_temps,
+) or pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --input --output",
+           -exitval => 3)
+    unless defined $input 
+    and defined $output;
+
+
+# The header kewords
+my $header = [
+        { name =>  'REQ_NAME', 
+                    writetype => TSTRING, 
+                    comment => 'Postage Stamp Request Name',
+                    value => undef
+        },
+        { name =>  'REQ_ID', 
+                    writetype => TLONGLONG, 
+                    comment => 'Postage Stamp Server Request id',
+                    value => undef
+        },
+];
+
+# Specification of columns to write
+my $columns = [ 
+        # rownum from original request
+        { name => 'ROWNUM',   type => 'V', writetype => TULONG }, 
+        # error code from processing this image
+        { name => 'ERROR_CODE',type => 'V',  writetype => TULONG }, 
+        # error string correspoding to ERROR_CODE
+        { name => 'ERROR_STR',type => '24A', writetype => TSTRING }, 
+        # name of the postage stamp image
+        { name => 'IMG_NAME', type => '64A', writetype => TSTRING }, 
+        # job_id that produced this result
+        { name => 'JOB_ID',   type => 'K',   writetype => TLONGLONG }, 
+        ### metadata from stamp
+        #
+        # coordinate at center of stamp, in degrees
+        { name => 'RA_DEG',  type => 'D',   writetype => TDOUBLE },
+        # coordinate at center of stamp, in degrees
+        { name => 'DEC_DEG', type => 'D',   writetype => TDOUBLE },
+
+        ### metdata from parent exposure
+
+        # actual start time of exposure
+        { name => 'MJD_OBS',  type => 'D',   writetype => TDOUBLE },
+        # field center at midpoint of expsure, in degrees
+        { name => 'RA_OBS',  type => 'D',   writetype => TDOUBLE },
+        # field center at midpoint of expsure, in degrees
+        { name => 'DEC_OBS', type => 'D',   writetype => TDOUBLE },
+        # actual filter
+        { name => 'FILTER',  type => '16A', writetype => TSTRING }, 
+        # exposure time of parent image
+        { name => 'EXPTIME', type => 'D',   writetype => TDOUBLE },
+        # original FPA_ID used at ingest
+        { name => 'FPA_ID',  type => '20A', writetype => TSTRING }, 
+
+        # the following are copied from the original pstamp request
+
+        # image selection parameters
+        { name => 'PROJECT',    type => '16A', writetype => TSTRING }, 
+        { name => 'JOB_TYPE',   type => '16A', writetype => TSTRING },     
+        { name => 'REQ_TYPE',   type => '16A', writetype => TSTRING },        
+        { name => 'IMG_TYPE',   type => '16A', writetype => TSTRING },       
+        { name => 'ID',         type => '16A', writetype => TSTRING },            
+        { name => 'TESS_ID',    type => '64A', writetype => TSTRING },    
+        { name => 'COMPONENT',  type => '64A', writetype => TSTRING },    
+        { name => 'DATA_GROUP', type => '64A', writetype => TSTRING },    
+
+        # output parameters
+        { name => 'OPTION_MASK',type => 'J', writetype   => TULONG },     
+
+        { name => 'MJD_MIN',   type => 'D', writetype   => TDOUBLE },
+        { name => 'MJD_MAX',   type => 'D', writetype   => TDOUBLE },       
+        { name => 'REQFILT',    type => '16A', writetype => TSTRING },
+
+        # 2 bits in COORD_MASK indicate what units of roi coords are
+        { name => 'COORD_MASK', type => 'J', writetype => TULONG },  
+        { name => 'CENTER_X',   type => 'D', writetype => TDOUBLE },    
+        { name => 'CENTER_Y',   type => 'D', writetype => TDOUBLE },   
+        { name => 'WIDTH',      type => 'D', writetype => TDOUBLE },   
+        { name => 'HEIGHT',     type => 'D', writetype => TDOUBLE },  
+        { name => 'COMMENT',    type => '64A', writetype => TSTRING },    
+];
+
+my $in;
+if ($input eq '-') {
+    $in = \*STDIN;
+} else {
+    open $in, "<$input" or die "cannot open $input for reading";
+}
+
+my @colData;
+foreach (@$columns) {
+    push @colData, [];
+}
+
+
+my $numRows = read_data_for_table($in,'\|', \@colData, $header); 
+if (!$numRows) {
+    print STDERR "no data in $input\n";
+    exit 1;
+}
+
+my $status = make_fits_table($output, EXTNAME, $numRows, \@colData, $columns, $header);
+
+exit $status;
+
+# TODO: put this in a module
+
+# two utility functions that may be used to create a FITS binary
+# table from hashes describing the header keywords and columns
+
+# read_table_description reads the data for a table from a simple text file
+# make_fits_table writes out the table to a named file
+
+
+# A function to build a fits binary table from supplied data 
+# 
+sub make_fits_table {
+        my $output = shift;     # name of output file
+        my $extname = shift;    # extension name
+        my $numRows = shift;    # number of rows in the table
+        my $colData = shift;    # ref to array of arrays containing the data for each column
+        my $columns = shift;    # ref to array of column descriptions (each a hash)
+                                # with keys: name, type, and writetype
+        my $header = shift;     # ref to array of header keyword descriptions - each a hash
+                                # with keys: name, name, writetype, comment, and value
+        my $status = 0;
+
+        die "incorrect arguments" if !defined($columns);
+        # note $header can be nil
+
+        # build arrays for cfitsio
+        my @colNames;			# Names of columns
+        my @colTypes;			# Types of columns
+        my @colWriteType;               # type to use to write
+
+        foreach my $colSpec ( @$columns) {
+            push @colNames, $colSpec->{name};
+            push @colTypes, $colSpec->{type};
+            push @colWriteType, $colSpec->{writetype};
+        }
+
+        if (-e $output) {
+            unlink "$output" or die "failed to remove existing $output";
+        }
+
+        my $outFits = Astro::FITS::CFITSIO::create_file( $output, $status ); # Output file handle
+        check_fitsio( $status );
+
+        $outFits->create_img( 16, 0, undef, $status );
+        check_fitsio( $status );
+
+        # Create the table
+
+        $outFits->create_tbl( BINARY_TBL(), $numRows, scalar @colNames,
+                                \@colNames, \@colTypes, undef, $extname, $status );
+        check_fitsio( $status );
+
+        # if header keyword descriptions were provided add them
+        if ($header) {
+            foreach my $headerword ( @$header ) {
+                my $value = $headerword->{value};
+                unless (defined $value) {
+                    print "Can't find header keyword $headerword\n";
+                    next;
+                }
+                # zap quotation marks
+                $value =~ s/\'//g;
+                my $name    = $headerword->{name};
+                my $type    = $headerword->{writetype};
+                my $comment = $headerword->{comment};
+                $outFits->write_key( $type, $name, $value, $comment, $status );
+                check_fitsio( $status );
+            }
+        }
+
+
+        for (my $i = 0; $i < scalar @colNames; $i++) {
+            my $writeType = $colWriteType[$i];
+            $outFits->write_col( $writeType, $i + 1, 1, 1, $numRows, $colData->[$i], $status );
+            check_fitsio( $status );
+        }
+
+        $outFits->close_file( $status );
+
+        return 0;
+
+} # end of sub make_fits_table
+
+
+
+# read the table contents from a file
+#
+# input text file format:
+#   lines that begin with '#' are comment lines and are skipped.
+#   other lines are data. Each data line is split into fields with the
+#   provided separator
+#
+# if $header is not null header the first non-commented line is read to
+# fill the value for each header keyword. The number of fields must match
+# the number of keywords.
+#
+# Following the optional header data, each data line contains data for each
+# row in the table. The number of fields must match the number of column
+# arrays provided.
+
+sub read_data_for_table {
+    my $in      = shift;    # input file handle
+    my $sep     = shift;    # string containing field separator
+    my $colData = shift;    # reference to an array of arrays for the data
+    my $header  = shift;    # rerence to array of header keyword descriptions
+
+    my $line_num = 0;
+
+    # read data for header if any data is expected
+    if ($header) {
+        my $nhead = @$header;
+        while (my $line = <$in>) {
+            $line_num++;
+            next if ($line =~ /^#/);    # skip comment lines
+            chomp $line;
+            my @vals = split /$sep/, $line;
+            my $nvals = @vals;
+            die "number of header columns in input $nvals does not equal expected number of header words $nhead"
+                    if (@vals != @$header);
+
+            for (my $i=0; $i < @$header; $i++) {
+                $header->[$i]->{value} = $vals[$i];
+            }
+
+            last; # only one header line
+        }
+    }
+
+    my $row_num = 0;
+    my $ncols = @$colData;
+    while (my $line = <$in>) {
+        $line_num++;
+        next if ($line =~ /^#/);    # skip comment lines
+        chomp $line;
+
+        my @vals = split /$sep/, $line;
+        my $nvals = @vals;
+        die "number of columns $nvals in input does not equal expected number of header "
+                . " words $ncols on line $line_num" if ($nvals != $ncols);
+
+        for (my $col = 0; $col < @$colData; $col++) {
+            $colData->[$col]->[$row_num] = $vals[$col];
+        }
+        $row_num++;
+    }
+
+    # we return the number of rows read
+    return $row_num;
+}
+
+# From Astro::FITS::CFITSIO demo
+sub check_fitsio
+{
+    my $status = shift;		# Status of FITSIO calls
+
+    if ($status != 0) {
+	my $msg;		# Message to output
+	Astro::FITS::CFITSIO::fits_get_errstatus( $status , $msg );
+	die "CFITSIO error: $msg\n";
+    }
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_revert_request.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_revert_request.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_revert_request.pl	(revision 41103)
@@ -0,0 +1,120 @@
+#!/bin/env perl
+
+# pstamp_finish.pl
+
+use warnings;
+use strict;
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+
+use Sys::Hostname;
+use IPC::Cmd 0.36 qw( can_run run );
+use File::Temp qw( tempfile );
+use File::Copy;
+
+use PS::IPP::Metadata::Config;
+use PS::IPP::Metadata::Stats;
+use PS::IPP::Metadata::List qw( parse_md_list );
+
+use PS::IPP::Config qw($PS_EXIT_SUCCESS
+		       $PS_EXIT_UNKNOWN_ERROR
+		       $PS_EXIT_SYS_ERROR
+		       $PS_EXIT_CONFIG_ERROR
+		       $PS_EXIT_PROG_ERROR
+		       $PS_EXIT_DATA_ERROR
+		       $PS_EXIT_TIMEOUT_ERROR
+		       metadataLookupStr
+		       metadataLookupBool
+		       caturi
+		       );
+
+
+my ( $req_id, $dbname, $dbserver, $verbose, $save_temps );
+
+GetOptions(
+	   'req_id=s'   => \$req_id,
+	   'dbname=s'   => \$dbname,
+	   'dbserver=s' => \$dbserver,
+	   'verbose'    => \$verbose,
+	   'save-temps' => \$save_temps,
+) or pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+
+pod2usage ( -msg => "--req_id is required", -exitval => 2 ) if !$req_id;
+
+my $missing_tools;
+
+my $pstamptool  = can_run('pstamptool')  or (warn "Can't find pstamptool"  and $missing_tools = 1);
+my $dsreg  = can_run('dsreg')  or (warn "Can't find dsreg"  and $missing_tools = 1);
+
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+my $ipprc = PS::IPP::Config->new(); # IPP Configuration
+
+my $outputDataStoreRoot = metadataLookupStr($ipprc->{_siteConfig}, 'DATA_STORE_ROOT');
+exit ($PS_EXIT_CONFIG_ERROR) unless defined $outputDataStoreRoot; # lookup failure outputs a message
+
+my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files
+
+my $req;
+{
+    my $command = "$pstamptool -listreq -req_id $req_id";
+    $command   .= " -dbname $dbname" if $dbname;
+    $command   .= " -dbserver $dbserver" if $dbserver;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        die("Unable to perform $command error code: $error_code");
+    }
+    my $output = join "", @$stdout_buf;
+    if (!$output) {
+        if ($verbose) {
+            print STDERR "no requests found\n"
+        }
+        exit 0;
+    }
+    my $metadata = $mdcParser->parse($output) or die("Unable to parse metdata config doc");
+
+    my $requests = parse_md_list($metadata);
+
+    $req = $requests->[0];
+
+    die "request $req_id not found in metadata config doc" if !$req;
+}
+
+my $product = $req->{outProduct};
+
+my $req_name = $req->{name};
+
+# set the output fileset's name to the request name.
+my $fileset = $req_name;
+
+print STDERR "product: $product  REQ_NAME: $req_name\n" if $verbose;
+
+if ($product and $fileset) {
+    my $command = "$dsreg --del $fileset --product $product --rm --force";
+#   don't add dbname let dsreg get it from the DS_DBNAME in site.config
+#    $command .= " --dbname $dbname" if $dbname;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        die "Unable to perform $command error code: $error_code";
+    }
+}
+
+
+{
+    my $command = "$pstamptool -revertreq -req_id $req_id";
+    $command   .= " -dbname $dbname" if $dbname;
+    $command   .= " -dbserver $dbserver" if $dbserver;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        die("Unable to perform $command error code: $error_code");
+    }
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_runcommand.sh
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_runcommand.sh	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_runcommand.sh	(revision 41103)
@@ -0,0 +1,66 @@
+#!/bin/sh
+###
+#
+# pstamp_runcommand.sh: 
+#
+# Set up the ipp environment to run a ipp commands on behalf of the postage stamp
+# server and execute the command given by the argument list 
+# This script is used by cgi or php scripts.
+
+# echo $# args are: $*
+
+if [[ $# -lt 4 ]] ;then
+    echo "usage $0 PSCONFDIR PSCONFIG WORKDIR command" >&2
+    #  EINVAL = 22
+    exit 22
+fi
+
+export PSCONFDIR=$1
+shift
+
+PSCONFIG=$1
+shift
+
+WORKDIR=$1
+shift
+export HOME=$WORKDIR
+
+
+cd $WORK_DIR
+status=$?
+if [[ $status != 0 ]] ; then
+    echo $0 cannot cd to WORK_DIR: $WORK_DIR status: $status >&2
+    exit $status
+fi
+
+###
+### configure the IPP
+###
+
+source $PSCONFDIR/psconfig.bash $PSCONFIG
+status=$?
+if [[ $status != 0 ]] ; then
+    echo error setting up IPP environment >&2
+    exit $status
+fi
+
+#echo path: $PATH >&2
+#echo command: $* >&2
+
+## make sure we are able to run the desired command
+## XXX: This test is sort of redundant. 
+## We'd get a more useful error by just going ahead and trying the command
+## or by not suppressing the error output of which
+if [[ ! -x `which $1 2> /dev/null` ]] ;then
+    status=$?
+    echo command $1 not found >&2
+    exit $status
+fi
+
+#### Finally, execute the command given by the command line arguments
+
+# make sure other users can modify files
+# todo: this is of course quite dangerous, avoid this by using proper group permissions
+umask 0
+
+$*
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_save_server_status.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_save_server_status.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_save_server_status.pl	(revision 41103)
@@ -0,0 +1,81 @@
+#!/bin/env perl
+#
+# run pstamp_server_status and save the results in a date stamped file
+# optionally change the symlink in the web directory to point to the new file
+
+use strict;
+use warnings;
+
+use PS::IPP::Config 1.01 qw( :standard );
+use IPC::Cmd 0.36 qw( can_run run );
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+
+my $updatelink;
+my $verbose;
+my $save_temps;
+my $pstamp_workdir;
+
+GetOptions(
+    'workdir=s'      => \$pstamp_workdir,
+    'update-link'    => \$updatelink,
+    'verbose'       => \$verbose,
+    'save-temps'    => \$save_temps,
+) or pod2usage( 2 );
+
+my $missing_tools;
+my $pstamp_server_status   = can_run('pstamp_server_status') 
+    or (warn "Can't find pstamp_server_status" and $missing_tools = 1);
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit($PS_EXIT_CONFIG_ERROR);
+}
+
+if (!$pstamp_workdir) {
+    my $ipprc = PS::IPP::Config->new(); # IPP Configuration
+    $pstamp_workdir = metadataLookupStr($ipprc->{_siteConfig}, 'PSTAMP_WORKDIR');
+}
+
+if (!$pstamp_workdir) {
+    warn("Failed to find PSTAMP_WORKDIR in the config\n");
+    exit($PS_EXIT_CONFIG_ERROR);
+}
+my $status_file = "$pstamp_workdir/server_status/status.html";
+
+my ($sec, $min, $hour, $mday, $month, $year) = gmtime;
+$year += 1900;
+$month += 1;
+
+my $dir = sprintf "$pstamp_workdir/server_status/%4d/%02d/%02d", $year, $month, $mday;
+
+if (!-e $dir ) {
+    my $rc = system "mkdir -p $dir";
+    if ($rc) {
+        my $status = $rc >> 8;
+        print STDERR "mkdir failed error: $status\n";
+        exit $status;
+    }
+}
+
+my $file = sprintf "$dir/pstamp.status.%4d-%02d-%02dT%02d:%02d:%02d.html",
+    $year, $month, $mday, $hour, $min, $sec;
+
+my $command = "pstamp_server_status --workdir $pstamp_workdir > $file";
+#my $command = "pstamp_server_status > $file";
+my  ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+    run(command => $command, verbose => $verbose);
+unless ($success) {
+    $error_code = (($error_code >> 8) or 1);
+    warn("$command failed. exit status: $error_code");
+    exit $error_code;
+}
+
+if ($updatelink) {
+    if (-e $status_file) {
+        unlink $status_file or die "failed to unlink existing status file $status_file\n";
+    }
+
+    symlink $file, $status_file or die "failed to update staus file link $status_file";
+}
+
+exit 0;
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_server_status
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_server_status	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_server_status	(revision 41103)
@@ -0,0 +1,201 @@
+#!/bin/env perl
+
+use strict;
+use warnings;
+
+use File::Temp qw( tempfile );
+
+#use DBI;
+use IPC::Cmd 0.36 qw( can_run run );
+#use PS::IPP::Metadata::Config;
+use PS::IPP::Config 1.01 qw( :standard );
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+
+
+my ($rundir, $workdir, $verbose, $save_temps);
+my $br = '<br />';
+
+GetOptions(
+    'rundir=s'      => \$rundir,
+    'workdir=s'     => \$workdir,
+    'verbose'       => \$verbose,
+    'save-temps'    => \$save_temps,
+) or pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+
+# Look for programs we need
+my $missing_tools;
+my $pantasks_client   = can_run('pantasks_client') 
+    or (warn "Can't find pantasks_client" and $missing_tools = 1);
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit($PS_EXIT_CONFIG_ERROR);
+}
+
+# XXX: Note under pantasks this will be the curent directory, so this
+# isn't necessary if the pstamp/web stuff is configured to point at the 
+# ipp build. But currently it's running out of Bill's build
+
+# EAM 2017.03.08 : need to point at temporary ITC location:
+# my $ipphome = "/home/panstarrs/ipp";
+my $ipphome = "/data/ippc64.1/ippitc";
+my $status_cmd = "psstatus";
+
+$rundir = "$ipphome/pstamp" if !$rundir;
+
+chdir $rundir or die "failed to cd to $rundir";
+
+# print "pantasks_client: $pantasks_client\n";
+
+my $now = `date -u`;
+
+print "<b>Status updated: &nbsp;&nbsp;&nbsp;</b> $now<br /><br />\n";
+
+my $down = 0;
+if ($down) {
+    print "Postage Stamp Server will be down for maintenance it will back at approximately 00:00 2010-12-11 UTC\n";
+    # exit 0;
+} else {
+    # talk to pantasks_server and get the status
+
+    my ($pts, $pantasks_script) = tempfile ('/tmp/pts.XXXX', UNLINK => !$save_temps);
+
+    print $pts "status\n";
+    print $pts "quit\n";
+    close $pts;
+
+
+    my $serverRunning = 0;
+    my $command = "$pantasks_client < $pantasks_script";
+    my  ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    if ($success) {
+        $serverRunning = 1;
+    } else {
+        $error_code = (($error_code >> 8) or 1);
+        if ($error_code == 12 || $error_code == 13) {
+            print "Postage Stamp Server is not running\n<br>";
+            #print "Postage Stamp Server will be down for maintenance it will back shortly.\n";
+#            exit 0;
+        } else {
+            warn("\nError: $command failed. exit status: $error_code\n");
+#            exit $error_code;
+        }
+    }
+
+    my $output = join "", @$stdout_buf;
+
+    print "$output" if $verbose;
+
+    my @lines = split "\n", $output;
+
+    my ($scheduler_state, $controller_state);
+    my $got_state = 0;
+    my %tasks;
+    foreach my $line (@lines) {
+        chomp $line;
+        next if !$line;
+        my ($first, $second, $third);
+
+        if ($line =~ /Scheduler is/) {
+            ($first, $second, $scheduler_state) = split " ", $line;
+            $got_state = 1;
+    #        print "$first $second $third $scheduler_state\n";
+        } elsif ($line =~ /Controller is/) {
+            ($first, $second, $controller_state) = split " ", $line;
+    #        print "$first $second $third $controller_state\n";
+        } elsif ($got_state == 1 and $line =~ /\+|\-/) {
+    #        print "$line\n";
+            my ($task_state, $task, $nrun, $njobs, $ngood, $nfail, $ntime, $cmd) = split " ", $line;
+    #        print "$task $task_state\n";
+            my %this_task = ( name => $task, nrun=> $nrun, njobs => $njobs, ngood => $ngood, nfail => $nfail, ntime => $ntime, cmd => $cmd ); 
+
+            # change '.' in task name to '_' so we have a valid hash key
+            $task =~ s/\./\_/g;
+            $tasks{$task} =  \%this_task;
+        }
+    }
+
+    if ($scheduler_state) {
+        print "Pantasks Scheduler $scheduler_state.\n";
+        if ($controller_state) {
+            print $br . "Pantasks Controller $controller_state.\n";
+        } else {
+            print STDERR "Controller state not found";
+#            exit 1;
+        }
+        print "$br$br\n";
+    } elsif ($serverRunning) {
+        print STDERR "Scheduler state not found";
+#        exit 1;
+    }
+
+    my $request_run = $tasks{'pstamp_request_run'};
+    my $request_fin = $tasks{'request_finish_run'};
+    my $job_run = $tasks{'pstamp_job_run'};
+
+
+    if ($request_run) {
+	# print "<br >\n";
+        print "<b>Parser Status:</b> $request_run->{nrun} running. $request_run->{ngood} parsed successfully.&nbsp; $request_run->{nfail} failed to parse.";
+        if ($request_fin) {
+            print "  $request_fin->{ngood} Requests finished.";
+        } else {
+            print "Task request.finish.run not found.\n";
+        }
+        print "\n";
+        print "\n";
+        if ($job_run) {
+            print "<br /><b>Job Status:</b> &nbsp;&nbsp; $job_run->{nrun} running. &nbsp; $job_run->{ngood} completed successfully. &nbsp; $job_run->{nfail} failed";
+            print "<br />\n";
+        } else {
+            print "Task pstamp.job.run not found.<br />\n";
+        }
+    } elsif ($serverRunning) {
+        print "Task pstamp.request.run not found.<br >\n";
+    }
+}
+if ($workdir) {
+    # get the space available in the working directory file system
+    my $df_output = `df -h $workdir`;
+    my @lines = split "\n", $df_output;
+    foreach my $line (@lines) {
+        next unless $line =~ /data/;
+        my ($total, $used, $free, $percent, $part) = split " ", $line;
+        print "<br><b>Server Working Directory:</b>&nbsp;&nbsp;$percent full. $free free out of $total total.<br>\n";
+    }
+
+}
+
+# now run the script psstatus to check the status of running requests and finished requests
+print "<br /><b>Unfinished Requests</b><br />\n";
+print "<pre>\n";
+system "$status_cmd --totals";
+print "</pre>\n";
+
+#print "<br />\n";
+
+
+# print "<br /><b>Unfinished Requests</b><br />\n";
+print "<pre>\n";
+system "$status_cmd --running";
+print "</pre>\n";
+
+if (0) {
+    # these are included in running requests above
+    print "<br /><b>Requests building results fileset</b><br/>\n";
+    print "<pre>\n";
+    system "$status_cmd --finishing";
+    print "</pre>\n";
+}
+
+print "<br /><b>Requests completed in last 24 hours (most recently completed first)</b><br />\n";
+print "<pre>\n";
+system "$status_cmd --finished";
+print "</pre>\n";
+
+
+exit 0;
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_targetlist_file.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_targetlist_file.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_targetlist_file.pl	(revision 41103)
@@ -0,0 +1,292 @@
+#!/bin/env perl
+
+# program to create a fits binary table with EXTNAME PS1_PS_TARGETLIST
+
+use warnings;
+use strict;
+
+use Astro::FITS::CFITSIO qw( :constants );
+Astro::FITS::CFITSIO::PerlyUnpacking(1);
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+use Math::Trig;
+use Data::Dumper;
+
+use constant EXTNAME => 'PS1_PS_TARGETLIST'; # Extension name for output table
+use constant EXTVER =>  2;
+
+my ( $input,			# Name of input Detectabilty Query table
+     $output,			# Name of output table
+     $save_temps,		# Save temporary files?
+     );
+
+GetOptions(
+	   'input|i=s'    => \$input,
+	   'output|o=s'   => \$output,
+	   'save-temps'   => \$save_temps,
+) or pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --input --output",
+           -exitval => 3)
+    unless defined $input 
+    and defined $output;
+
+
+# The header kewords
+my $header = [
+        { name =>  'REQ_NAME', 
+                    writetype => TSTRING, 
+                    comment => 'Postage Stamp Request Name',
+                    value => undef
+        },
+        { name =>  'REQ_ID', 
+                    writetype => TLONGLONG, 
+                    comment => 'Postage Stamp Server Request id',
+                    value => undef
+        },
+];
+
+# Specification of columns to write
+my $columns = [ 
+        # rownum from original request
+        { name => 'ROWNUM',   type => 'V', writetype => TULONG }, 
+
+        # single exposure results
+        { name => 'FPA_ID',  type => '20A', writetype => TSTRING }, 
+        { name => 'EXP_ID',  type => 'V',   writetype => TULONG },
+        { name => 'CHIP_ID', type => 'V',   writetype => TULONG },
+        { name => 'CHIP_READY', type => '2A',  writetype => TSTRING},
+#        { name => 'CAM_ID',  type => 'V',   writetype => TULONG },
+        { name => 'WARP_ID', type => 'V',   writetype => TULONG },
+        { name => 'WARP_READY', type => '2A',  writetype => TSTRING},
+
+        # stack columns
+        { name => 'STACK_ID', type => 'V',  writetype => TULONG },
+        { name => 'STACK_FAILED', type => '2A',  writetype => TSTRING},
+        { name => 'SEEING',   type => 'D',   writetype => TDOUBLE },
+
+        # stack and diff specific columns
+        { name => 'TESS_ID',  type => '64A', writetype => TSTRING },    
+        { name => 'SKYCELL_ID', type => '64A', writetype => TSTRING },    
+        { name => 'RUN_TYPE', type => '64A', writetype => TSTRING },    
+
+        # actual start time of exposure
+        { name => 'MJD_OBS',  type => 'D',   writetype => TDOUBLE },
+        # field center at midpoint of expsure, in degrees
+        { name => 'RA_OBS',  type => 'D',   writetype => TDOUBLE },
+        # field center at midpoint of expsure, in degrees
+        { name => 'DEC_OBS', type => 'D',   writetype => TDOUBLE },
+        # actual filter
+        { name => 'FILTER',  type => '16A', writetype => TSTRING }, 
+        # exposure time of parent image
+        { name => 'EXPTIME', type => 'D',   writetype => TDOUBLE },
+
+        # the following are copied from the original pstamp request
+
+        # image selection parameters
+        { name => 'PROJECT',    type => '16A', writetype => TSTRING }, 
+        { name => 'SURVEY_NAME',type => '64A', writetype => TSTRING },    
+        { name => 'IPP_RELEASE',type => '64A', writetype => TSTRING },    
+        { name => 'REQ_TYPE',   type => '16A', writetype => TSTRING },        
+        { name => 'IMG_TYPE',   type => '16A', writetype => TSTRING },       
+        { name => 'ID',         type => '16A', writetype => TSTRING },            
+        { name => 'DATA_GROUP', type => '64A', writetype => TSTRING },    
+
+        # error code from processing this row
+        { name => 'ERROR_CODE',type => 'V',  writetype => TULONG }, 
+        # error string correspoding to ERROR_CODE
+        { name => 'ERROR_STR',type => '24A', writetype => TSTRING }, 
+
+        # from original request
+        { name => 'COMMENT',    type => '64A', writetype => TSTRING },    
+
+];
+
+my $in;
+if ($input eq '-') {
+    $in = \*STDIN;
+} else {
+    open $in, "<$input" or die "cannot open $input for reading";
+}
+
+my @colData;
+my $i = 0;
+foreach my $col (@$columns) {
+    print "$i $col->{name}\n";
+    push @colData, [];
+    $i++;
+}
+
+
+my $numRows = read_data_for_table($in,'\|', \@colData, $header); 
+if (!$numRows) {
+    print STDERR "no data in $input\n";
+    exit 1;
+}
+
+my $status = make_fits_table($output, EXTNAME, $numRows, \@colData, $columns, $header);
+
+exit $status;
+
+# TODO: put this in a module
+
+# two utility functions that may be used to create a FITS binary
+# table from hashes describing the header keywords and columns
+
+# read_table_description reads the data for a table from a simple text file
+# make_fits_table writes out the table to a named file
+
+
+# A function to build a fits binary table from supplied data 
+# 
+sub make_fits_table {
+        my $output = shift;     # name of output file
+        my $extname = shift;    # extension name
+        my $numRows = shift;    # number of rows in the table
+        my $colData = shift;    # ref to array of arrays containing the data for each column
+        my $columns = shift;    # ref to array of column descriptions (each a hash)
+                                # with keys: name, type, and writetype
+        my $header = shift;     # ref to array of header keyword descriptions - each a hash
+                                # with keys: name, name, writetype, comment, and value
+        my $status = 0;
+
+        die "incorrect arguments" if !defined($columns);
+        # note $header can be nil
+
+        # build arrays for cfitsio
+        my @colNames;			# Names of columns
+        my @colTypes;			# Types of columns
+        my @colWriteType;               # type to use to write
+
+        foreach my $colSpec ( @$columns) {
+            push @colNames, $colSpec->{name};
+            push @colTypes, $colSpec->{type};
+            push @colWriteType, $colSpec->{writetype};
+        }
+
+        if (-e $output) {
+            unlink "$output" or die "failed to remove existing $output";
+        }
+
+        my $outFits = Astro::FITS::CFITSIO::create_file( $output, $status ); # Output file handle
+        check_fitsio( $status );
+
+        $outFits->create_img( 16, 0, undef, $status );
+        check_fitsio( $status );
+
+        # Create the table
+
+        $outFits->create_tbl( BINARY_TBL(), $numRows, scalar @colNames,
+                                \@colNames, \@colTypes, undef, $extname, $status );
+        check_fitsio( $status );
+
+        # if header keyword descriptions were provided add them
+        if ($header) {
+            foreach my $headerword ( @$header ) {
+                my $value = $headerword->{value};
+                unless (defined $value) {
+                    print "Can't find header keyword $headerword\n";
+                    next;
+                }
+                # zap quotation marks
+                $value =~ s/\'//g;
+                my $name    = $headerword->{name};
+                my $type    = $headerword->{writetype};
+                my $comment = $headerword->{comment};
+                $outFits->write_key( $type, $name, $value, $comment, $status );
+                check_fitsio( $status );
+            }
+        }
+
+
+        for (my $i = 0; $i < scalar @colNames; $i++) {
+            my $writeType = $colWriteType[$i];
+            $outFits->write_col( $writeType, $i + 1, 1, 1, $numRows, $colData->[$i], $status );
+            check_fitsio( $status );
+        }
+
+        $outFits->close_file( $status );
+
+        return 0;
+
+} # end of sub make_fits_table
+
+
+
+# read the table contents from a file
+#
+# input text file format:
+#   lines that begin with '#' are comment lines and are skipped.
+#   other lines are data. Each data line is split into fields with the
+#   provided separator
+#
+# if $header is not null header the first non-commented line is read to
+# fill the value for each header keyword. The number of fields must match
+# the number of keywords.
+#
+# Following the optional header data, each data line contains data for each
+# row in the table. The number of fields must match the number of column
+# arrays provided.
+
+sub read_data_for_table {
+    my $in      = shift;    # input file handle
+    my $sep     = shift;    # string containing field separator
+    my $colData = shift;    # reference to an array of arrays for the data
+    my $header  = shift;    # rerence to array of header keyword descriptions
+
+    my $line_num = 0;
+
+    # read data for header if any data is expected
+    if ($header) {
+        my $nhead = @$header;
+        while (my $line = <$in>) {
+            $line_num++;
+            next if ($line =~ /^#/);    # skip comment lines
+            chomp $line;
+            my @vals = split /$sep/, $line;
+            my $nvals = @vals;
+            die "number of header columns in input $nvals does not equal expected number of header words $nhead"
+                    if (@vals != @$header);
+
+            for (my $i=0; $i < @$header; $i++) {
+                $header->[$i]->{value} = $vals[$i];
+            }
+
+            last; # only one header line
+        }
+    }
+
+    my $row_num = 0;
+    my $ncols = @$colData;
+    while (my $line = <$in>) {
+        $line_num++;
+        next if ($line =~ /^#/);    # skip comment lines
+        chomp $line;
+
+        my @vals = split /$sep/, $line;
+        my $nvals = @vals;
+        die "number of columns $nvals in input does not equal expected number of header "
+                . " words $ncols on line $line_num" if ($nvals != $ncols);
+
+        for (my $col = 0; $col < @$colData; $col++) {
+            $colData->[$col]->[$row_num] = $vals[$col];
+        }
+        $row_num++;
+    }
+
+    # we return the number of rows read
+    return $row_num;
+}
+
+# From Astro::FITS::CFITSIO demo
+sub check_fitsio
+{
+    my $status = shift;		# Status of FITSIO calls
+
+    if ($status != 0) {
+	my $msg;		# Message to output
+	Astro::FITS::CFITSIO::fits_get_errstatus( $status , $msg );
+	die "CFITSIO error: $msg\n";
+    }
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_webrequest.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_webrequest.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstamp_webrequest.pl	(revision 41103)
@@ -0,0 +1,155 @@
+#!/bin/env perl
+###
+#
+# pstampwebrequest.pl: take a postage stamp request command line and process it
+#
+# The arguments are the command line parameters for the program psmkreq
+#
+# Note: Despite the name there nothing particularly web specific about this program.
+#
+###
+
+use warnings;
+use strict;
+
+use Getopt::Long qw( GetOptions );
+use Sys::Hostname;
+use POSIX qw( strftime );
+
+my $host = hostname();
+my $verbose = 0;
+my $dbname;
+my $dbserver;
+my $pretend;
+my $label = 'WEB';
+my $req_name_prefix = 'web';
+my $username;
+
+GetOptions(
+    'dbname=s'      =>  \$dbname,
+    'dbserver=s'    =>  \$dbserver,
+    'label=s'       =>  \$label,
+    'username=s'    =>  \$username,
+    'prefix=s'      =>  \$req_name_prefix,
+    'pretend'       =>  \$pretend,
+    'verbose'       =>  \$verbose,
+);
+
+
+
+if ($verbose) {
+    print STDERR "\n\n";
+    print STDERR "Starting script $0 on $host\n\n";
+}
+
+
+use IPC::Cmd 0.36 qw( can_run run );
+
+use PS::IPP::Config qw($PS_EXIT_SUCCESS
+		       $PS_EXIT_UNKNOWN_ERROR
+		       $PS_EXIT_SYS_ERROR
+		       $PS_EXIT_CONFIG_ERROR
+		       $PS_EXIT_PROG_ERROR
+		       $PS_EXIT_DATA_ERROR
+		       $PS_EXIT_TIMEOUT_ERROR
+		       metadataLookupStr
+		       metadataLookupBool
+		       caturi
+		       );
+
+use Cwd;
+
+my $missing_tools;
+
+my $psmkreq = can_run('psmkreq')  or (warn "Can't find psmkreq"  and $missing_tools = 1);
+my $pstamptool = can_run('pstamptool')  or (warn "Can't find pstamptool"  and $missing_tools = 1);
+my $pstampparse = can_run('pstampparse.pl')  or (warn "Can't find pstampparse.pl"  and $missing_tools = 1);
+
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+# make a request file in a sub directory of the current directory
+my $cur_dir = getcwd();
+
+#print STDERR "cur_dir is $cur_dir\n";
+
+# put file in directory for the current date
+my $datestr = strftime "%Y/%m/%d", gmtime;
+my $datedir = "$cur_dir/webreq/$datestr";
+if (! -e $datedir ) {
+    my $rc = system "mkdir -p $datedir";
+    if ($rc) {
+        my $status = $rc >> 8;
+        print STDERR  "failed to create working directory $datedir: $rc $status";
+        exit $PS_EXIT_CONFIG_ERROR;
+    }
+}
+
+my $request_name = $req_name_prefix . '_' . get_webreq_num();
+my $request_file = "$datedir/$request_name.fits";
+{
+    my $command = "$psmkreq --req_name $request_name  --output $request_file @ARGV";
+
+if (0) {
+open DEBUG, ">>/tmp/pstamp.debug.log";
+print DEBUG "\ncommand is: $command\n";
+close DEBUG;
+}
+
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        print STDERR @$stderr_buf;
+        print STDERR "\ncommand was: $command\n";
+        die("Unable to perform psmkreq: $error_code");
+    }
+}
+
+# Queue the request
+my $req_id = 0;
+unless ($pretend) {
+
+    my $command = "$pstamptool -addreq -uri $request_file -ds_id 0";
+    $command .= " -label $label";
+    $command .= " -username $username" if $username;
+    $command .= " -dbname $dbname" if $dbname;
+    $command .= " -dbserver $dbserver" if $dbserver;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        print STDERR @$stderr_buf;
+        die("Unable to perform pstamptool -addreq: $error_code");
+    }
+    $req_id = ${$stdout_buf}[0];
+    chomp $req_id;
+}
+
+print "$req_id $request_name";
+
+exit 0;
+
+# Ask the database for the next web request number
+sub get_webreq_num
+{
+    my $command = "$pstamptool -getwebrequestnum";
+    $command .= " -dbname $dbname" if $dbname;
+    $command .= " -dbserver $dbserver" if $dbserver;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        print STDERR @$stderr_buf;
+        die("Unable to perform pstamptool -getwebrequestnum: $error_code");
+    }
+    my $webreq_num = ${$stdout_buf}[0];
+    chomp $webreq_num;
+
+    if (!$webreq_num) {
+        die("pstamptool -getwebreqnum returned no value");
+    }
+
+    # print STDERR "webreq_num $webreq_num\n";
+
+    return $webreq_num;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstampparse.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstampparse.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstampparse.pl	(revision 41103)
@@ -0,0 +1,1422 @@
+#!/bin/env perl
+###
+### pstampparse.pl
+###
+###     Run the postage stamp request parser for a given request id
+###
+
+use warnings;
+use strict;
+
+use Sys::Hostname;
+use Getopt::Long qw( GetOptions );
+use PS::IPP::PStamp::RequestFile qw( :standard );
+use PS::IPP::PStamp::Job qw( :standard );
+use File::Temp qw(tempfile);
+use File::Basename qw(basename);
+use Carp;
+use POSIX;
+use Time::HiRes qw(gettimeofday);
+use Time::Local;
+
+my $verbose;
+my $dbname;
+my $dbserver;
+my $req_id;
+my $request_file_name;
+my $mode = "list_uri";
+my $outdir;
+my $product;
+my $label = "";
+my $save_temps;
+my $no_update;
+my $dest_requires_magic;
+my $dump_params = 0;
+my $new_email;
+
+my %accessLevelCache;
+
+# set this to true to disable update processing
+my $no_updates_allowed = 0;
+
+GetOptions(
+    'file=s'    =>  \$request_file_name,
+    'req_id=s'  =>  \$req_id,
+    'outdir=s'  =>  \$outdir,
+    'product=s' =>  \$product,
+    'label=s'   =>  \$label,
+    'new_email=s' => \$new_email,
+    'mode=s'    =>  \$mode,
+    'need_magic'=>  \$dest_requires_magic,
+    'dbname=s'  =>  \$dbname,
+    'dbserver=s'=>  \$dbserver,
+    'verbose'   =>  \$verbose,
+    'save-temps'=>  \$save_temps,
+    'no-update' =>  \$no_update,
+    'dump-params' => \$dump_params,
+);
+
+die "invalid mode '$mode'" unless ($mode eq "list_uri") or ($mode eq "queue_job");
+die "--file is required"   unless defined($request_file_name);
+die "req_id is required"  unless defined $req_id;
+if ($mode ne 'list_uri') {
+    die "outdir is required"  if !$outdir;
+} else {
+    $outdir = "somewhere" if !$outdir
+}
+
+use IPC::Cmd 0.36 qw( can_run run );
+
+use PS::IPP::Metadata::Config;
+use PS::IPP::Metadata::Stats;
+use PS::IPP::Metadata::List qw( parse_md_list );
+
+use PS::IPP::Config qw( :standard );
+my $ipprc = PS::IPP::Config->new(); # IPP Configuration
+
+my $missing_tools;
+
+my $pstamptool  = can_run('pstamptool')  or (warn "Can't find pstamptool"  and $missing_tools = 1);
+my $pstampdump  = can_run('pstampdump') or (warn "Can't find pstampdump" and $missing_tools = 1);
+my $fields      = can_run('fields') or (warn "Can't find fields" and $missing_tools = 1);
+
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+# just deal with these arguments once and for all
+$pstamptool .= " -dbname $dbname" if $dbname;
+$pstamptool .= " -dbserver $dbserver" if $dbserver;
+
+# If $mode is not queue_job we are using a debugging mode
+# do not update the database
+$no_update = 1 if $mode ne "queue_job";
+
+$product = 'NULL' unless $product;
+
+
+# look up some defaults
+my $defaultAccessLevel = metadataLookupS32($ipprc->{_siteConfig}, 'PSTAMP_DEFAULT_ACCESS_LEVEL');
+if (!defined $defaultAccessLevel) {
+    warn("cannot find 'PSTAMP_DEFAULT_ACCESS_LEVEL' in site config");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+# look up the default data store product.
+my $defaultDSProduct = metadataLookupStr($ipprc->{_siteConfig}, 'PSTAMP_DATA_STORE_PRODUCT');
+if (!defined $defaultDSProduct) {
+    # Don't panic yet that it's not defined.
+    #  exit ($PS_EXIT_CONFIG_ERROR);
+    $defaultDSProduct = ''; # set to empty string to simplify some comparisions below
+    print STDERR "Warning PSTAMP_DATA_STORE_PRODUCT not found in the site config.\n" if  $product eq 'NULL';
+}
+
+my $productIsDefault = 0;
+if ($product and $defaultDSProduct) {
+    $productIsDefault = ($product eq $defaultDSProduct);
+}
+
+my $mdcParser = PS::IPP::Metadata::Config->new; # Parser for metadata config files
+
+#
+# Read the keywords from the extension header
+#
+my $fields_output;
+{
+    my $command = "echo $request_file_name | $fields -x 0 EXTNAME EXTVER REQ_NAME ACTION EMAIL";
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+
+    # note fields doesn't return zero when it succeeds.
+    $fields_output = join "", @$stdout_buf;
+}
+my (undef, $extname, $extver, $req_name, $action, $email) = split " ", $fields_output;
+
+# make sure the file contains what we are expecting
+# pstamp_parser_run.pl would not have run this program unless the request file was ok
+my_die("$request_file_name does not contain EXTNAME\n", $PS_EXIT_PROG_ERROR) if !$extname;
+my_die("$request_file_name is not a PS1_PS_REQUEST\n", $PS_EXIT_PROG_ERROR) if $extname ne "PS1_PS_REQUEST";
+my_die("REQ_NAME not found in $request_file_name\n", $PS_EXIT_PROG_ERROR)  if (!$req_name);
+my_die("wrong EXTVER $extver found in $request_file_name\n", $PS_EXIT_PROG_ERROR) if ($extver ne "1" and $extver ne "2");
+
+if ($extver eq "1") {
+    print STDERR "Version 1 postage stamp requests are no longer accepted. Please update your request tables to version 2 format.\n";
+    print STDERR "Note that EMAIL will not be optional\n";
+
+    # if need be we can hack Peter Veres' email in here if label = 'WEB.UP' and req_name like 'Peter_Veres%'
+
+} elsif (!$email) {
+    my_die("ERROR: Required parameter EMAIL not found in request file header.\n", $PSTAMP_INVALID_REQUEST) 
+}
+
+
+    
+
+if ($extver >= 2) {
+    # We have a version 2 file. Require that the new keywords be supplied. 
+    my_die("action not supplied in version $extver request file $request_file_name\n", $PSTAMP_INVALID_REQUEST) 
+        unless defined $action;
+
+    my_die("invalid action: $action supplied in version $extver request file $request_file_name\n",
+        $PSTAMP_INVALID_REQUEST)
+        unless (uc($action) eq 'PROCESS' or uc($action) eq 'PREVIEW');
+
+    my_die("email not supplied in version $extver request file $request_file_name\n", $PSTAMP_INVALID_REQUEST)
+        unless $email;
+} else {
+    # for version 1 file the action is process and email is not used
+    $action = 'PROCESS';
+    $email  = 'null';
+}
+
+print "Request Header Keywords EXTVER: $extver REQ_NAME: $req_name ACTION: $action EMAIL: $email\n";
+
+if ($new_email) {
+    # this is primarily for ease in testing
+    print "Setting email to command line provided value $new_email\n";
+    $email = $new_email
+}
+
+# check for duplicate request name
+my $duplicate_req_name = 0;
+if ($req_id and !$no_update) {
+    my $command = "$pstamptool -listreq  -name $req_name -not_req_id $req_id";
+    # no verbose so that error message about request not found doesn't appear in parse_error.txt
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => 0);
+    my $exitStatus = $error_code >> 8;
+    if ($success) {
+        # -listreq succeeded there is already a request in the database with this name
+        print STDERR "REQ_NAME $req_name has already been used\n";
+        insertFakeJobForRow(undef, 0, $PSTAMP_DUP_REQUEST);
+        $duplicate_req_name = 1;
+        my $datestr = strftime "%Y%m%d%H%M%S.$req_id", gmtime;
+        $req_name = "ERROR.$datestr";
+        #exit 0;
+    }
+}
+
+
+
+my $label_changed = 0;
+if (0) {
+# Adjust the label for requests coming in over the web upload interface
+# XXX: do this later based on the user information
+# We now use the access tabels to set the label.
+if ($label and $label eq "WEB.UP") {
+    my $lcname = lc($req_name);
+    if ($lcname =~ /pitt/) {
+        $label = "PITT";
+        $label_changed = 1;
+    } elsif ($lcname =~ /cfa/) {
+        $label = "CFA";
+        $label_changed = 1;
+    } elsif ($lcname =~ /durham/) {
+        $label = "DURHAM";
+        $label_changed = 1;
+    } elsif ($lcname =~ /qub/) {
+        $label = "QUB";
+        $label_changed = 1;
+    } elsif ($lcname =~ /pypstamp/) {
+        $label = "PY";
+        $label_changed = 1;
+    } elsif ($lcname =~ /ncu/) {
+    	$label = 'NCU';
+	$label_changed = 1;
+    } elsif (($lcname =~ /mpe_/) or ($lcname =~ /sd_/) or ($lcname =~ /^jk/)) {
+    	$label = 'MPE';
+	$label_changed = 1;
+    }
+    print "Setting label for $req_name to $label\n" if $label_changed;
+}
+}
+
+# based on the user's email address get the accessLevel, default label and data store product
+my $productForUser;
+my $labelForUser = $label;  # XXX: for now I pass the current label for use in hacking permissions for V1 requests.
+
+my $accessLevel = getUsersAccessLevel($email, \$labelForUser, \$productForUser);
+if ($accessLevel < 0) {
+    print STDERR "Data access is forbidden for $email.\n";
+    insertFakeJobForRow(undef, 0, $PSTAMP_NOT_AUTHORIZED);
+}
+
+# if the request came through the request table upload channel, change it to the user's specific label
+# if any is known. Do we want to do this for any other channels?
+# requests from data store will have those values.
+if ($label and ($label eq 'WEB.UP' or $label eq 'WEB')) {
+    if ($labelForUser) {
+        $label = $labelForUser;
+        $label_changed = 1;
+    }
+}
+
+# If the product is the system default, set it to the product for the user@domain. That way 
+# if the request came through a data store that target will be used.
+# We really only need to redirect the results with greater access level than the default.
+# note that for most users productForUser will be undef so the default will be used.
+if ($productIsDefault or $product eq 'NULL') {
+    if ($productForUser) {
+        $product = $productForUser;
+    }
+} else {
+    # if we don't have a product use default
+    if ($product eq 'NULL') {
+        $product = $defaultDSProduct;
+    }
+}
+
+if (!$product or $product eq 'NULL') {
+    # this will only happen if the site.config variable is missing.
+    my_die("No product found.\n", $PS_EXIT_CONFIG_ERROR);
+}
+
+{
+    # update the request's row with the new parameters.
+    # The requset name will be used to set the output fileset name in the output data store.
+    my $command = "$pstamptool -updatereq -req_id $req_id  -set_name $req_name";
+    $command .= " -set_username $email" if $email ne 'null';
+    $command .= " -set_outProduct $product";
+    $command .= " -set_label $label" if $label_changed;
+    unless ($no_update) {
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+        unless ($success) {
+            my_die("$command failed", $PS_EXIT_UNKNOWN_ERROR);
+        }
+    } else {
+        print "skipping $command\n";
+    }
+}
+
+# in these two error conditions we are done. The invoking script will finish up.
+if ($duplicate_req_name) {
+    exit 0;
+}
+
+if ($accessLevel < 0) {
+    exit 0;
+}
+
+
+#
+# now convert the request table to an array of metadatas
+#
+
+my $rows;
+{
+    my $start_request_file = gettimeofday();
+    my $command = "$pstampdump $request_file_name";
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        print STDERR @$stderr_buf;
+    }
+    if (@$stdout_buf) {
+        $rows = parse_md_fast($mdcParser, join "", @$stdout_buf);
+    }
+    my $dtime_request_file = gettimeofday() - $start_request_file;
+    print "Time to read and parse request file: $dtime_request_file\n";
+}
+
+my $nRows = $rows ? scalar @$rows : 0;
+print "\n$nRows rows read from request file\n";
+
+unless ($nRows) {
+    # pstamp_job_run was invoked so the request file must have contained a valid header
+    # a request file with no rows is invalid.
+    # The print above will let the log file know a bit more.
+    # Insert a faulted fake job and exit this program successfully.
+    print STDERR "Invalid request file.\n";
+    insertFakeJobForRow(undef, 0, $PSTAMP_INVALID_REQUEST);
+    exit 0;
+}
+
+
+# if label is for one of the high priority channels, watch for big requests
+my $watch_for_big_requests = (!($label =~ /BIG/) and ($label =~ /PSI/ or $label =~ /WEB/));
+
+# XXX: these should be in a configuration file somewhere not hard coded
+my $big_limit = 100;
+my $job_big_limit = 400;
+
+if ($watch_for_big_requests and $nRows > $big_limit) {
+    $label = change_to_lower_priority_label($label);
+    $watch_for_big_requests = 0;
+}
+
+my $num_jobs = 0;
+
+
+foreach my $row (@$rows) {
+
+    # validate the paramaters
+    if (!checkRow($row)) {
+        # when it enconters an error checkRow adds a fake job with an appropriate error code to the database
+        $num_jobs++;
+        next;
+    }
+    # initialize counter for "job number"
+    $row->{job_num} = 0;
+    $row->{error_code} = 0;
+    $row->{accessLevel} = $accessLevel;
+
+    $num_jobs += processRow($action, $row);
+
+    # see whether number of jobs limit for high priority request was exceeded
+    if ($watch_for_big_requests and $num_jobs > $job_big_limit) {
+        $label = change_to_lower_priority_label($label);
+        $watch_for_big_requests = 0;
+    }
+}
+
+if (($action eq 'LIST' or $mode eq "queue_job") and ($num_jobs eq 0)) {
+    # this should not happen. The processRow is required to insert a fake job for any
+    # rows that did not yield any jobs.
+    print STDERR "ERROR: zero jobs created for $req_name\n";
+    insertFakeJobForRow(undef, 0, $PS_EXIT_PROG_ERROR);
+}
+
+exit 0;
+
+# end of main function
+
+sub change_to_lower_priority_label {
+    my $label = shift;
+    my $old_label = $label;
+    $label = ($label =~ /WEB/) ? 'WEB.BIG' : 'PSI.BIG';
+    print "\nChanging label for big $old_label request to $label\n";
+
+    my $command = "$pstamptool -updatereq -req_id $req_id  -set_label $label";
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        my_die("$command failed", $PS_EXIT_UNKNOWN_ERROR);
+    }
+    return $label;
+}
+
+sub checkRow {
+        
+    my $row = shift;
+
+    # check validity of the paramters in a request specification
+    # If we encounter an error for a particular row add a job with the proper fault code.
+    # also adjust some paramterers like filter and set defaults for some others
+
+    my $stage = $row->{IMG_TYPE};
+
+    my $rownum   = $row->{ROWNUM};
+    if (!validID($rownum)) {
+	$rownum = 'NULL' if !defined $rownum;
+        print STDERR "$rownum is not a valid ROWNUM\n";
+        insertFakeJobForRow($row, 1, $PSTAMP_INVALID_REQUEST);
+        return 0;
+    }
+    my $job_type = $row->{JOB_TYPE};
+    if (!defined $job_type || (($job_type ne "stamp") and ($job_type ne "get_image"))) {
+    	$job_type = 'NULL' if !defined $job_type;
+        print STDERR "$job_type is not a valid JOB_TYPE\n";
+        insertFakeJobForRow($row, 1, $PSTAMP_INVALID_REQUEST);
+        return 0;
+    }
+    
+    my $req_type = $row->{REQ_TYPE};
+    if (($req_type ne "byid") and ($req_type ne "bycoord") and ($req_type ne "byexp") and
+        ($req_type ne "byskycell") and ($req_type ne "bydiff")) {
+	$req_type = 'NULL' if !defined $req_type;
+        print STDERR "$req_type is not a valid REQ_TYPE\n";
+        insertFakeJobForRow($row, 1, $PSTAMP_INVALID_REQUEST);
+        return 0;
+    }
+    if ($job_type eq 'get_image') {
+        # get_image jobs are quite expensive in terms of space so we are currently restricting them
+        unless ($req_type eq 'byid' or $req_type eq 'byexp' 
+            or ($req_type eq 'byskycell' and $stage eq 'stack')
+            or ($stage eq 'stack_summary')) {
+            print STDERR "REQ_TYPE must be 'byid' or 'byexp' JOB_TYPE 'get_image' for IMG_TYPE $stage\n";
+            insertFakeJobForRow($row, 1, $PSTAMP_INVALID_REQUEST);
+            return 0;
+        }
+    }
+
+    my $component = $row->{COMPONENT};
+    if (!defined $component or (lc($component) eq "null") or (lc($component) eq "all")) {
+        if ($job_type eq 'get_image' and ! ($stage eq 'stack' or $stage eq 'stack_summary')) {
+            $row->{COMPONENT} = 'all';
+        } else {
+            $row->{COMPONENT} = $component = "";
+        }
+    }
+    $row->{TESS_ID} = "" if !defined $row->{TESS_ID};
+
+    my $filter  = $row->{REQFILT};
+    if ($filter) {
+        if (length($filter) == 1) {
+            # allow single character filter cuts to work
+            $row->{REQFILT} .= '%';
+        }
+    }
+    my $mjd_min = $row->{MJD_MIN};
+    if (defined($mjd_min) and !validNumber($mjd_min)) {
+        print STDERR "$mjd_min is not a valid MJD_MIN\n";
+        insertFakeJobForRow($row, 1, $PSTAMP_INVALID_REQUEST);
+        return 0;
+    }
+    my $mjd_max = $row->{MJD_MAX};
+    if (defined($mjd_max) and !validNumber($mjd_max)) {
+        print STDERR "$mjd_max is not a valid MJD_MAX\n";
+        insertFakeJobForRow($row, 1, $PSTAMP_INVALID_REQUEST);
+        return 0;
+    }
+
+    my $fwhm_min = $row->{FWHM_MIN};
+    if (!defined $fwhm_min) {
+        $row->{FWHM_MIN} = 0;
+    } elsif (!validNumber($fwhm_min)) {
+        print STDERR "$fwhm_min is not a valid FWHM_MIN\n";
+        insertFakeJobForRow($row, 1, $PSTAMP_INVALID_REQUEST);
+        return 0;
+    }
+    my $fwhm_max = $row->{FWHM_MAX};
+    if (!defined $fwhm_max) {
+        $row->{FWHM_MAX} = 0;
+    } elsif (!validNumber($fwhm_max)) {
+        print STDERR "$fwhm_max is not a valid FWHM_MAX\n";
+        insertFakeJobForRow($row, 1, $PSTAMP_INVALID_REQUEST);
+        return 0;
+    }
+
+    my $data_group = $row->{DATA_GROUP};
+    if (!defined $data_group) {
+        # backwards compatability hook
+        $data_group = $row->{LABEL};
+        $data_group = "null" if !defined $data_group;
+        $row->{DATA_GROUP} = $data_group;
+    }
+        
+    # req_finish doesn't work if bit zero of option mask is not set;
+    $row->{OPTION_MASK} |= 1;
+
+    my $option_mask= $row->{OPTION_MASK};
+    my $inverse = ($option_mask & $PSTAMP_SELECT_INVERSE) ? 1 : 0;
+    $row->{inverse} = $inverse;
+    my $unconvolved = ($option_mask & $PSTAMP_SELECT_UNCONV) ? 1 : 0;
+    $row->{unconvolved} = $unconvolved;
+
+    my $skycenter = $row->{skycenter} = ! ($row->{COORD_MASK} & $PSTAMP_CENTER_IN_PIXELS);
+
+    my $wholefile = 0;
+    if (($row->{WIDTH} == 0 && $row->{HEIGHT} == 0) ||
+       (!$skycenter && $row->{CENTER_X} == 0 && $row->{CENTER_Y} == 0)) {
+        # Secret code for returning the whole file
+        $wholefile = 1;
+    }
+
+    if (!$skycenter and !$wholefile and !$component and $stage ne 'stack' and !($option_mask & $PSTAMP_USE_IMFILE_ID)){
+        print STDERR "COMPONENT must be specified for pixel coordinate ROI center\n";
+        insertFakeJobForRow($row, 1, $PSTAMP_INVALID_REQUEST);
+        return 0;
+    }
+
+    if (!check_image_type($stage)) {
+        print STDERR "invalid IMG_TYPE for row $rownum\n";
+        insertFakeJobForRow($row, 1, $PSTAMP_INVALID_REQUEST);
+        return 0;
+    }
+
+    if ((($job_type eq "stamp") or ($req_type eq "bycoord")) and ! validROI($row)) {
+        print STDERR "invalid ROI for row $rownum\n";
+        insertFakeJobForRow($row, 1, $PSTAMP_INVALID_REQUEST);
+        return 0;
+    }
+
+    if (($req_type eq "byexp") and ($stage eq "stack" or $stage eq 'stack_summary')) {
+        print STDERR "byexp not implemented for $stage stage. row: $rownum\n";
+        insertFakeJobForRow($row, 1, $PSTAMP_NOT_IMPLEMENTED);
+        return 0;
+    }
+
+    if ($req_type eq "bycoord") {
+        if (!$skycenter) {
+            print STDERR "center must be specified in sky coordintes for bycoord";
+            insertFakeJobForRow($row, 1, $PSTAMP_INVALID_REQUEST);
+            return 0;
+        }
+    }
+
+    if (($req_type eq "byid") or ($req_type eq "bydiff")) {
+        if (!validID($row->{ID})) {
+            print STDERR "ID must be a positive integer for req_type $req_type\n";
+            insertFakeJobForRow($row, 1, $PSTAMP_INVALID_REQUEST);
+            return 0
+        }
+    }
+
+
+    return 1;
+}
+
+sub list_targets {
+    my $rowList = shift;
+    my $num_jobs = 0;
+
+    $num_jobs = 1;
+    return $num_jobs;
+}
+
+sub processRow {
+    my $action  = shift;
+    my $row     = shift;
+
+    my $num_jobs = 0;
+
+    my $project  = $row->{PROJECT};
+
+    # note: resolve_project avoids running pstamptool every time by remembering the
+    # last project resolved
+    my $proj_hash = resolve_project($ipprc, $project, $dbname, $dbserver);
+    if (!$proj_hash) {
+        insertFakeJobForRow($row, 1, $PSTAMP_UNKNOWN_PROJECT);
+        $num_jobs++;
+        return $num_jobs;
+    }
+    my $image_db   = $proj_hash->{dbname};
+    my $camera     = $proj_hash->{camera};
+
+    my $req_type    = $row->{REQ_TYPE};
+    my $rownum      = $row->{ROWNUM};
+
+    my $mjd_max;
+    my $adjustedDateCuts = adjustDateCuts($row, \$mjd_max);
+    if ($adjustedDateCuts and $mjd_max < 0) {
+        print STDERR "User is not authorized for data from this project.\n";
+        insertFakeJobForRow($row, 1, $PSTAMP_NOT_AUTHORIZED);
+        $num_jobs++;
+        return $num_jobs;
+    }
+
+    # Since user can get unmagicked data "by coordinate" requests can go back in time
+    # to dredge unusable data from the "dark days"...
+    if ($req_type eq 'bycoord' and $row->{IMG_TYPE} ne 'stack' and $row->{MJD_MIN} == 0) {
+        # ... so unless the user sets mjd_min clamp it to 2009-04-01
+        # XXX: This value should live in the pstampProject table not be hardcoded here
+        $row->{MJD_MIN} = 54922;
+    }
+
+    if (($req_type eq 'byskycell') or ($req_type eq 'bycoord')) {
+        # avoid error from print below if $id isn't needed
+        $row->{ID} = "" if !$row->{ID};
+
+        # if this user does not have unlimited access rights adjust the date cut
+        # to avoid more recent data
+        # We do not adjust dates for stacks since they are not well defined enough.
+        # We'll need to filter them by accessLevel during the lookup
+        my $stage = $row->{IMG_TYPE};
+        if ($stage ne 'stack' and $adjustedDateCuts) {
+            # if lower limit was supplied by user we can leave the value alone
+            if ($row->{MJD_MAX}) {
+                if ($row->{MJD_MAX} > $mjd_max) {
+                    print STDERR "Access rights require limiting MJD_MAX to $mjd_max\n";
+                    $row->{MJD_MAX} = $mjd_max;
+                }
+            } else {
+                # no limit supplied we silently adjust it
+                $row->{MJD_MAX} = $mjd_max;
+            }
+        }
+    }
+    
+    # Call PS::IPP::PStamp::Job locate_images subroutine to get the images for this
+    # request specification. An array reference is returned.
+    my $start_locate = gettimeofday();
+
+    print "\nCalling locate_images_for_row for row: $rownum\n";
+
+    my $imageList = locate_images_for_row($ipprc, $image_db, $camera, $row, $verbose);
+
+    my $dtime_locate = gettimeofday() - $start_locate;
+    print "Time to locate_images for row $rownum $dtime_locate\n";
+
+    my @rowList = ($row);
+    $num_jobs += queueJobs($action, \@rowList, $imageList);
+
+    # if this row slipped through without a job being added add one. 
+    if ($row->{job_num} == 0) {
+        print "row $row->{ROWNUM} produced no jobs\n";
+        print STDERR "row $row->{ROWNUM} produced no jobs\n";
+        my $error_code = $row->{error_code};
+        $error_code =  $PSTAMP_NO_IMAGE_MATCH if !$error_code;
+        insertFakeJobForRow($row, ++$row->{job_num}, $error_code);
+    }
+
+    return $num_jobs;
+}
+
+sub queueJobForImage
+{
+    my $row = shift;
+    my $stage = shift;
+    my $image = shift;
+    my $need_magic = shift;
+    my $action = shift;
+
+    my $rownum = $row->{ROWNUM};
+    my $option_mask = $row->{OPTION_MASK};
+    my $components = $row->{components};
+
+    $option_mask |= $PSTAMP_NO_WAIT_FOR_UPDATE if $no_updates_allowed;
+
+    # determine if there is a cut off date for data access for this user
+    my $mjd_max;
+    my $adjustedDateCuts = adjustDateCuts($row, \$mjd_max);
+
+    my $roi_string;
+
+    # note values were checked by the function validROI()
+    my $x = $row->{CENTER_X};
+    my $y = $row->{CENTER_Y};
+    my $w = $row->{WIDTH};
+    my $h = $row->{HEIGHT};
+    my $coord_mask = $row->{COORD_MASK};
+
+    my $wholeFile = 0; 
+    # For historical reasons there are two ways to specify that the entire file be returned 
+    # rather than a postage stamp ...
+    if ($w == 0 and $h == 0) {
+        # ... The right way: width and height both zero ...
+        $wholeFile = 1;
+    } else {
+        if ($coord_mask & $PSTAMP_CENTER_IN_PIXELS) {
+            if ($x == 0 && $y == 0) {
+                # ... and pixel coordinate center of 0, 0
+                # I made this one up without thinking through the API clearly.
+                # allowing width and height to do it works much better
+                $wholeFile = 1;
+            } else {
+                $roi_string = "-pixcenter $x $y";
+            }
+        } else {
+            $roi_string = "-skycenter $x $y";
+        }
+    }
+
+    if ($wholeFile) {
+        $roi_string = "-wholefile";
+    } elsif ($coord_mask & $PSTAMP_RANGE_IN_PIXELS) {
+            $roi_string .= " -pixrange $w $h";
+    } else {
+            $roi_string .= " -arcrange $w $h";
+    }
+
+    my $component = $image->{component};
+
+    my $job_num = ++($row->{job_num});
+
+    my $imagefile = $image->{image};
+
+    if ($stage eq "stack") {
+        # unconvolved stack images weren't available prior to some point in time.
+        # XXX: handle this more correctly by examining the stack run's config dump file.
+        # It looks like # the feature was turned on sometime around November 11, 2009. stackRun 30067 is the lowest
+        # one that I found with an unconvolved image.
+        my $MIN_GPC1_STACK_ID_WITH_UNCONVOLVED_IMAGES = 30067;
+        if ($row->{unconvolved} and ($row->{PROJECT} eq 'gpc1') and 
+            ($image->{stack_id} < $MIN_GPC1_STACK_ID_WITH_UNCONVOLVED_IMAGES)) {
+            print STDERR "Unconvolved stack image is not available for stackRun.stack_id: $image->{stack_id}\n";
+            insertFakeJobForRow($row, $job_num, $PSTAMP_NOT_AVAILABLE);
+            return 1;
+        }
+    } else { 
+        if ($need_magic and !$image->{magicked}) {
+            # XXX: should we add a faulted job so the client can know what happened if no images come back?
+            # The test for destreaked is made in locate_images now so this code never runs. This leads to no feedback
+            # to users, but speeds up processing significantly
+            print STDERR "skipping non-magicked image $imagefile\n" if $verbose;
+
+            # for now assume yes.
+
+            insertFakeJobForRow($row, $job_num, $PSTAMP_NOT_DESTREAKED);
+            return 1;
+        }
+
+        if ($adjustedDateCuts) {
+            my $mjd = 0;
+            my $exp_id;
+            if ($stage eq 'diff') {
+                # diff stage doesn't have a dateobs so we need to hack it.
+                # Extract dateobs from the exp_name_1
+                if ($row->{PROJECT} eq 'gpc1') {
+                    my $exp_name = $image->{exp_name_1};
+                    if ($exp_name ne 'NULL') {
+                        $mjd = 50000 + (substr($exp_name, 1, 4));
+                        $exp_id = $image->{exp_id_1};
+                    } else {
+                        # it's stack-stack diff just let it go with mjd = 0
+                        # XXX: deal with this
+                    }
+                }
+            } else {
+                $exp_id = $image->{exp_id};
+                $mjd = dateobsToMJD($image->{dateobs});
+            }
+
+            if ($mjd > $mjd_max) {
+                print "User is not authorized for stamps from this exposure exp_id $exp_id.\n";
+                print STDERR "User is not authorized for stamps from this exposure exp_id $exp_id.\n";
+                insertFakeJobForRow($row, $job_num, $PSTAMP_NOT_AUTHORIZED, $exp_id);
+                return 1;
+            }
+        }
+    }
+    my $exp_id = $image->{exp_id};
+            
+    my $args = $roi_string ? $roi_string : "";
+    if ($stage eq "raw" or $stage eq "chip") {
+        $args .= " -class_id $component" if $component;
+    }
+
+    $image->{job_args} = $args;
+
+    my $base = basename($image->{image});
+    if (! $base =~ /.fits$/ ) {
+        my_die("unexpected image file name found $image->{image}", $PS_EXIT_PROG_ERROR);
+    }
+    $base =~ s/.fits$//;
+
+    my $filter = $image->{filter};
+    if (!$filter) {
+        if ($stage eq 'diff') {
+            $filter = $image->{filter_1};
+        }
+        if (!$filter) {
+            # XXX: perhaps this should be a programming error...
+            print STDERR "missing filter using 'X'\n";
+            $filter = 'X';
+        }
+    }
+    # use first character of filter
+    $filter = substr($filter, 0, 1);
+            
+    my $output_base = "$outdir/${rownum}_${job_num}_${filter}_${base}";
+    write_params($output_base, $image);
+
+    my $newState = $action eq 'PROCESS' ? "run" : "parsed";
+    my $fault = 0;
+    my $dep_id;
+
+    queueUpdatesIfNeeded($action, $stage, $image, $option_mask, \$newState, \$fault, \$dep_id);
+
+    my $command = "$pstamptool -addjob  -req_id $req_id -job_type $row->{JOB_TYPE}"
+                    . " -outputBase $output_base -rownum $rownum -state $newState -options $option_mask";
+    $command .= " -fault $fault" if $fault;
+    $command .= " -exp_id $exp_id" if $exp_id;
+    $command .= " -dep_id $dep_id" if $dep_id;
+
+    if (!$no_update) {
+        # mode eq "queue_job"
+        my $start_addjob = gettimeofday();
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+        unless ($success) {
+            print STDERR @$stderr_buf;
+            # XXX TODO: now what? Should we mark the error state for the request?
+            # should we keep going for other uris? If so how do we report that some
+            # of the work that the request wanted isn't going to get done
+            my_die("failed to queue job for request $req_id", $PS_EXIT_UNKNOWN_ERROR);
+        }
+        my $dtime_addjob = gettimeofday() - $start_addjob;
+        print "Time to addjob for row $rownum $dtime_addjob\n";
+    } else {
+        print "skipping command: $command\n";
+    }
+
+    return 1;
+}
+
+# queue jobs for a collection of request specifications that have the same Images of Interest
+sub queueJobs
+{
+    my $action = shift;
+    my $rowList = shift;
+    my $imageList = shift;
+
+    my $firstRow = $rowList->[0];
+    my $stage    = $firstRow->{IMG_TYPE};
+    my $job_type = $firstRow->{JOB_TYPE};
+    my $need_magic = $firstRow->{need_magic};
+
+    my $num_jobs = 0;
+
+    if ($mode eq "list_uri") {
+        $num_jobs = $imageList ? scalar @$imageList : 0;
+        print "List of $num_jobs Images selected for row: $firstRow->{ROWNUM}\n";
+        foreach my $image (@$imageList) {
+            print "$image->{image}\n";
+            ++$firstRow->{job_num};
+            if ($dump_params) {
+                my $rownum = $firstRow->{ROWNUM};
+                my $jobnum = $firstRow->{job_num};
+                my $filter = substr $image->{filter}, 0, 1;
+                my $output_base = "${rownum}_${jobnum}_${filter}";
+                write_params($output_base, $image);
+            }
+        }
+    } elsif ($job_type eq "get_image") {
+        my $n = scalar @$rowList;
+
+        my_die( "error: unexpected number of rows for get_image request: $n", $PS_EXIT_PROG_ERROR) if $n != 1;
+
+        $num_jobs = queueGetImageJobs($firstRow, $imageList, $stage, $need_magic, $action);
+
+    } else {
+        if (!$imageList or (scalar @$imageList eq 0)) {
+            # We didn't find any images for this set of rows. Insert a fake job to carry
+            # the status back to the requestor.
+            foreach my $row (@$rowList) {
+                my $error_code = $row->{error_code};
+                $error_code = $PSTAMP_NO_IMAGE_MATCH if !$error_code;
+                insertFakeJobForRow($row, ++$row->{job_num}, $error_code);
+                $num_jobs++;
+            }
+            return $num_jobs;
+        }
+
+        foreach my $image (@$imageList) {
+            # get the array of row indices that touch this image
+            my $row_index = $image->{row_index};
+            if (!$row_index or scalar @$row_index == 0) {
+                # XXX should this happen? Why did something get returned.
+                print "image ${stage}_id: $image->{stage_id} component: $image->{component} matched no rows\n";
+                next;
+            }
+            # XXX: TODO: eventually we may change ppstamp to be able to make multiple stamps per invocation
+
+            foreach my $i (@$row_index) {
+                my $row = $rowList->[$i];
+
+                $num_jobs += queueJobForImage($row, $stage, $image, $need_magic, $action);
+            }
+        }
+    }
+
+    return $num_jobs;
+}
+
+sub queueGetImageJobs
+{
+    my $row = shift;
+    my $imageList = shift;
+    my $stage = shift;
+    my $need_magic = shift;
+    my $action = shift;
+
+    my $num_jobs = 0;
+    my $rownum = $row->{ROWNUM};
+    my $option_mask = $row->{OPTION_MASK};
+
+    $option_mask |= $PSTAMP_NO_WAIT_FOR_UPDATE if $no_updates_allowed;
+
+    # For dist_bundle we need
+    #  --camera from $image
+    #  --stage 
+    #  --stage_id from $image
+    #  --component from $image
+    #  --path_base 
+    #  --outdir global to this script
+
+    # loop over images
+    foreach my $image (@$imageList) {
+        my $stage_id = $image->{stage_id};
+        my $component = $image->{component};
+
+        # skip faulted components for now. Should we even be here?
+        if ($image->{fault} > 0) {
+            printf STDERR "skipping faulted component for $stage $stage_id $component\n" if $verbose;
+            next;
+        }
+
+        my $job_num = ++($row->{job_num});
+
+        my $imagefile = $image->{image};
+        if (($stage ne "stack") and ($need_magic and !$image->{magicked})) {
+            # we only get here if req_type is (byid or byexp). For other types the test for magicked is performed
+            # in locate_images because it's much more efficient to do the test in the database.
+            # For these two modes we fall through to here in order to give feedback to the requestor as
+            # to why the request failed to queue jobs.
+            print STDERR "skipping non-magicked image $imagefile\n" if $verbose;
+            insertFakeJobForRow($row, $job_num, $PSTAMP_NOT_DESTREAKED);
+            $num_jobs++;
+
+            next;
+        }
+        my $exp_id = $image->{exp_id};
+            
+        my $output_base = "$outdir/${rownum}_${job_num}";
+
+        write_params($output_base, $image);
+
+        my $newState = $action eq 'PROCESS' ? "run" : "parsed";
+        my $fault = 0;
+        my $dep_id;
+
+        queueUpdatesIfNeeded($action, $stage, $image, $option_mask, \$newState, \$fault, \$dep_id);
+
+        $num_jobs++;
+        my $command = "$pstamptool -addjob  -req_id $req_id -job_type $row->{JOB_TYPE}"
+                        . " -outputBase $output_base -rownum $rownum -state $newState -options $option_mask";
+        $command .= " -fault $fault" if $fault;
+        $command .= " -exp_id $exp_id" if $exp_id;
+        $command .= " -dep_id $dep_id" if $dep_id;
+
+        if (!$no_update) {
+            # mode eq "queue_job"
+            my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+                run(command => $command, verbose => $verbose);
+            unless ($success) {
+                print STDERR @$stderr_buf;
+                # XXX TODO: now what? Should we mark the error state for the request?
+                # should we keep going for other uris? If so how do we report that some
+                # of the work that the request wanted isn't going to get done
+                my_die("failed to queue job for request $req_id", $PS_EXIT_UNKNOWN_ERROR);
+            }
+        } else {
+            print "skipping command: $command\n";
+        }
+    }
+    if ( $num_jobs == 0 ) {
+        print STDERR "no jobs for row $rownum\n" if $verbose;
+        insertFakeJobForRow($row, 1, $PSTAMP_NO_IMAGE_MATCH);
+        $num_jobs = 1;
+	$row->{job_num} = 1;
+    }
+    return $num_jobs;
+}
+sub insertFakeJobForRow
+{
+    my $row = shift;
+    my $job_num = shift;
+    my $fault = shift;
+    my $exp_id = shift;
+
+    my ($job_type, $rownum);
+    if ($row) {
+        $job_type = $row->{JOB_TYPE};
+        $rownum = $row->{ROWNUM};
+        $rownum = 0 if !defined $rownum;
+        if ($job_type) {
+            if (($job_type ne "stamp") and ($job_type ne "get_image")) {
+                print STDERR "invalid job type: $job_type found in row $rownum\n";
+                $job_type = "none";
+            }
+        } else {
+            print STDERR "undefined job type found in row $rownum\n";
+            $job_type = "none";
+        }
+    } else {
+        $job_type = "none";
+        $rownum = 0;
+    }
+
+    my $command = "$pstamptool -addjob  -req_id $req_id -job_type $job_type"
+                        . " -rownum $rownum -state stop -fault $fault";
+    $command .= " -exp_id $exp_id" if $exp_id and $exp_id ne 'NULL';
+
+    if (!$no_update) {
+        # mode eq "queue_job"
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+        unless ($success) {
+            print STDERR @$stderr_buf;
+            # XXX TODO: now what? Should we mark the error state for the request?
+            # should we keep going for other uris? If so how do we report that some
+            # of the work that the request wanted isn't going to get done
+            my_die("failed to queue job for request $req_id", $PS_EXIT_UNKNOWN_ERROR);
+        }
+    } else {
+        print "skipping command: $command\n";
+    }
+}
+
+sub same_images_of_interest {
+    my $r1 = shift;
+    my $r2 = shift;
+
+    return 0 if (($r1->{REQ_TYPE} eq "bycoord")   or ($r2->{REQ_TYPE} eq "bycoord"));
+    return 0 if (($r1->{JOB_TYPE} eq "get_image") or ($r2->{JOB_TYPE} eq "get_image"));
+    return 0 if ($r1->{REQ_TYPE} ne $r2->{REQ_TYPE});
+    return 0 if ($r1->{IMG_TYPE} ne $r2->{IMG_TYPE});
+    return 0 if ($r1->{ID}       ne $r2->{ID});
+    return 0 if ($r1->{TESS_ID}  ne $r2->{TESS_ID});
+    return 0 if ($r1->{COMPONENT}  ne $r2->{COMPONENT});
+    return 0 if ($r1->{REQFILT}  ne $r2->{REQFILT});
+    return 0 if ($r1->{DATA_GROUP}    ne $r2->{DATA_GROUP});
+    return 0 if ($r1->{MJD_MIN}  ne $r2->{MJD_MAX});
+    return 0 if ($r1->{MJD_MAX}  ne $r2->{MJD_MAX});
+    return 0 if ($r1->{OPTION_MASK}  ne $r2->{OPTION_MASK});
+    return 0 if ($r1->{PROJECT}  ne $r2->{PROJECT});
+    return 0 if ($r1->{inverse}  ne $r2->{inverse});
+    return 0 if ($r1->{unconvolved}  ne $r2->{unconvolved});
+    # don't combine requests in pixel coordinates
+    return 0 if (($r1->{COORD_MASK} & $PSTAMP_CENTER_IN_PIXELS) || ($r2->{COORD_MASK} & $PSTAMP_CENTER_IN_PIXELS));
+
+    return 1;
+}
+
+sub validNumber
+{
+    my $val = shift;
+
+    return 0 if !defined $val;
+
+    return ($val =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/);
+}
+sub validID
+{
+    my $val = shift;
+
+    return 0 if !$val;
+
+    return  ! ($val =~ /\D/);
+}
+
+sub validROI
+{
+    my $row = shift;
+    return 0 if !validNumber($row->{CENTER_X});
+    return 0 if !validNumber($row->{CENTER_Y});
+    return 0 if !validNumber($row->{WIDTH});
+    return 0 if !validNumber($row->{HEIGHT});
+
+    return 1;
+}
+
+sub findRow
+{
+    my $rownum = shift;
+    my $rowList = shift;
+
+    foreach my $row (@$rowList) {
+        return $row if $row->{ROWNUM} eq $rownum;
+    }
+    return undef;
+}
+
+sub get_dependent 
+{
+    my ($action, $r_jobState, $r_fault, $r_dep_id, $imagedb, $state, $stage, $stage_id, $component, $need_magic) = @_;
+
+    # chipRun's can be in full state if destreaking is necessary
+    if (($state ne 'cleaned') and ($state ne 'update') and ($state ne 'goto_cleaned') and 
+        ($stage ne 'chip' and $state eq 'full')) {
+        my_die("$stage $stage_id is in unexpected state $state", $PS_EXIT_PROG_ERROR);
+    }
+
+    if (($stage eq 'diff') and ($stage_id <= 22778)) {
+    	print STDERR "diff_id $stage_id cannot be updated\n";
+	$$r_dep_id = 0;
+	$$r_fault = $PSTAMP_GONE;
+	$$r_jobState = 'stop';
+	return;
+    }
+
+    my $dep_id;
+    my $command = "$pstamptool -getdependent -stage $stage -stage_id $stage_id -imagedb $imagedb -component $component -outdir $outdir";
+    $command .= " -need_magic" if $need_magic;
+#    $command .= ' -hold' if $action eq 'PREVIEW';
+
+    if ($label) {
+        # compute rlabel for the run.
+        # XXX: This bit of policy shouldn't be buried so deeply in the code
+        # For now use one that implies 'postage stamp server' 'update' 'request_label"
+        my $rlabel = "ps_ud_" . $label;
+        $command .= " -rlabel $rlabel";
+    }
+
+    if (!$no_update) {
+        my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+            run(command => $command, verbose => $verbose);
+        unless ($success) {
+            my $fault = $error_code >> 8;
+            print STDERR "$command failed with fault $fault\n";
+            if ($fault < $PSTAMP_FIRST_ERROR_CODE) {
+                # pstamptool returns an error if an existing depenent is faulted
+                # Set the object to not available even if the fault < $PSTAMP_FIRST_ERROR_CODE
+                # which is nominally a recoverable error in order to keep
+                # the request from faulting (which can be very expensive if
+                # there are a lot of requests)
+                $fault = $PSTAMP_NOT_AVAILABLE
+            }
+            if ($fault >= $PSTAMP_FIRST_ERROR_CODE) {
+                $$r_dep_id = 0;
+                $$r_fault = $fault;
+                $$r_jobState = 'stop';
+                return;
+            }
+            # for now just die. Later pstamptool will return the whole dependent so we can
+            # examine the fault code
+            my_die("$command failed with unexpected fault value: $fault", $PS_EXIT_UNKNOWN_ERROR);
+        } else {
+            my $output = join "", @$stdout_buf;
+            chomp $output;
+            $dep_id = $output;
+            #
+            # XXX: need to fault the request or something
+            my_die("pstamptool -getdependent returned invalid dep_id", $PS_EXIT_PROG_ERROR) if !$dep_id;
+        }
+    } else {
+        print STDERR "skipping $command\n";
+        $dep_id = 42;
+    }
+    
+    $$r_dep_id = $dep_id;
+    $$r_fault = 0;
+    $$r_jobState = 'run';
+}
+
+sub queueUpdatesIfNeeded {
+    my $action = shift;
+    my $stage = shift;
+    my $image = shift;
+    my $option_mask = shift;
+    my $r_newState = shift;
+    my $r_fault = shift;
+    my $r_dep_id = shift;
+
+    my $need_magic = 0;
+
+    if ($stage ne 'raw') {
+        my $run_state = $image->{state};
+        my $data_state = $image->{data_state};
+        $data_state = $run_state if $stage eq 'stack';
+        my $component_fault = $image->{fault};
+        my $stage_id = $image->{stage_id};
+        my $component = $image->{component};
+
+        if ($component_fault eq $PSTAMP_GONE) {
+            # image is gone and it's not coming back
+            print STDERR "$stage $stage_id $component is gone\n";
+            $$r_newState = 'stop';
+            $$r_fault = $PSTAMP_GONE;
+        } elsif (($run_state =~ /purged/) or ($run_state =~ /scrubbed/) or ($run_state eq 'drop')) {
+            # image is gone and it's not coming back
+            print STDERR "$stage $stage_id $component has state: $run_state data_state: $data_state\n";
+            $$r_newState = 'stop';
+            $$r_fault = $PSTAMP_GONE;
+        } elsif (($run_state eq "error_cleaned") or ($data_state eq 'error_cleaned')) {
+            # if cleanup had an error don't get the user's hopes up set fault to gone
+            print STDERR "$stage $stage_id $component has state: $run_state data_state: $data_state\n";
+            $$r_newState = 'stop';
+            $$r_fault = $PSTAMP_GONE;
+        } elsif (($data_state ne 'full') or ($need_magic and ($image->{magicked} < 0)) or ($run_state eq 'goto_cleaned')) {
+   
+            if ($stage eq 'stack') {
+                print STDERR "Stamps cannot be made from cleaned stacks.\n";
+                $$r_newState = 'stop';
+                $$r_fault = $PSTAMP_GONE;
+            }
+            if (!$$r_fault) {
+                # wait for update unless the customer asks us not to
+                if (($option_mask & $PSTAMP_NO_WAIT_FOR_UPDATE)) {
+                    $$r_newState = 'stop';
+                    $$r_fault = $PSTAMP_NOT_AVAILABLE;
+                } elsif ($need_magic and !$image->{magicked}) {
+                    $$r_newState = 'stop';
+                    $$r_fault = $PSTAMP_NOT_DESTREAKED;
+                } else {
+                    # cause the image to be re-made
+                    # set up to queue an update run
+                    my $require_magic = ($need_magic or $image->{magicked});
+                    get_dependent($action, \$$r_newState, \$$r_fault, $r_dep_id, $image->{imagedb}, 
+                        $run_state, $stage, $image->{stage_id}, $image->{component}, $require_magic );
+                }
+            }
+        }
+    }
+}
+
+sub write_params {
+    my $output_base = shift;
+    my $image = shift;
+
+    # write the contents of this "image" as a metadata config doc
+    # Simply treat all values as strings. This is ok since we are only going to
+    # read it from another perl script
+    my $mdc_file = "${output_base}.mdc";
+    open P, ">$mdc_file" or my_die("failed to open $mdc_file", $PS_EXIT_UNKNOWN_ERROR);
+
+    print P "params METADATA\n";
+
+    foreach my $key (keys %$image) {
+        my $value = $image->{$key};
+        if (defined $value) {
+            printf P "  %-20s STR     %s\n", $key, $value;
+        } else {
+            printf P "  %-20s STR     NULL\n", $key;
+        }
+    }
+
+    print P "END\n";
+    close P or my_die("failed to close $mdc_file", $PS_EXIT_UNKNOWN_ERROR);
+}
+
+sub check_image_type
+{
+    my $img_type = shift;
+    if (!$img_type) {
+	    print STDERR "NULL IMG_TYPE supplied\n";
+	    return 0;
+    }
+    if (($img_type eq "raw") or ($img_type eq "chip") or ($img_type eq "warp") or
+	($img_type eq "stack") or ($img_type eq 'stack_summary') or ($img_type eq "diff")) {
+	return 1;
+    } else {
+	print STDERR "$img_type is not a valid IMG_TYPE\n";
+	return 0;
+    }
+}
+
+# get user's access level default label and output product
+sub getUsersAccessLevel {
+    my $email = shift;
+    my $r_label = shift;
+    my $r_product = shift;
+
+    # $r_label is a copy of the initial label. We change it to a user specific one
+    # if one is found.
+    my $label = $$r_label;
+
+    # default settings
+    $$r_label   =  undef;
+    $$r_product =  undef;
+    my $level = -1;
+
+    if (!$email) {
+        # No email provided. 
+        # For now in order to temporarily continue to support the version 1 request format.
+        # set the access rights based on the assigned label.
+        # Before deployment this will be forbidden.
+        if ($label eq 'IFA' or $label eq 'QUB' or $label =~ 'MOPS') {
+            $level = 2;
+        } else {
+            $level = $defaultAccessLevel;
+        }
+        return $level
+    }
+
+    # we've got an email check the domain
+    my ($user, $domain) = split '@', $email;
+    unless ($user and $domain) {
+        print STDERR "Error $email is not an acceptable email adddress.\n";
+        return -1;
+    } 
+
+    my $cmd = "$pstamptool -listuser -user $user -domain $domain";
+    my $results = runToolAndParse($cmd, $verbose);
+    my $userinfo = $results->[0];
+    if ($userinfo) {
+        $level = $userinfo->{accessLevel};
+        if ($userinfo->{userProduct}) {
+            $$r_product = $userinfo->{userProduct};
+        } elsif ($userinfo->{domainProduct}) {
+            $$r_product = $userinfo->{domainProduct};
+        }
+        if ($userinfo->{userLabel}) {
+            $$r_label = $userinfo->{userLabel};
+        } elsif ($userinfo->{domainLabel}) {
+            $$r_label = $userinfo->{domainLabel};
+        }
+    } else {
+        my $cmd = "$pstamptool -listdomain -domain $domain";
+        my $results = runToolAndParse($cmd, $verbose);
+        my $domaininfo = $results->[0];
+        if ($domaininfo) {
+            $level = $domaininfo->{accessLevel};
+            if ($domaininfo->{defaultProduct}) {
+                $$r_product = $domaininfo->{defaultProduct};
+            }
+            if ($domaininfo->{defaultLabel}) {
+                $$r_label = $domaininfo->{defaultLabel};
+            }
+    } else {
+            # no specific user or domain information found in the database. Use the defaults
+            $level = $defaultAccessLevel;
+            $$r_label =  undef;
+            $$r_product =  undef;
+        }
+    }
+
+    return $level;
+}
+
+
+# determine the latest exposure date that a user may have access to
+# based on the user's accessLevel.
+sub adjustDateCuts {
+    my $row = shift;
+    my $r_mjd_max = shift;
+    my $adjusted = 0;
+
+    my $accessLevel = $row->{accessLevel};
+    my $project = $row->{PROJECT};
+    my $mjd_max_user = $row->{MJD_MAX};
+
+    my $key = "$project.$accessLevel";
+
+    my $levelInfo = $accessLevelCache{$key};
+
+    if (!$levelInfo) {
+        my $cmd = "$pstamptool -listaccesslevel -accessLevel $accessLevel -project_name $project";
+        my $results = runToolAndParse($cmd, $verbose);
+        if (!$results or !$results->[0]) {
+            my_die("failed to find accessLevel info for project: $project level: $accessLevel\n", $PS_EXIT_CONFIG_ERROR);
+        }
+        $levelInfo = $results->[0];
+        $accessLevelCache{$key} = $levelInfo;
+    }
+
+    my $mjd_max_thisLevel = $levelInfo->{mjd_max};
+    if ($mjd_max_thisLevel)  {
+        # This project has a limit for this level
+        # adjust the user supplied limit unless it is less than the maximum
+
+        if (!$mjd_max_user or ($mjd_max_user > $mjd_max_thisLevel)) {
+            $$r_mjd_max = $mjd_max_thisLevel;
+            $adjusted = 1;
+        }
+    }
+
+    return $adjusted;
+}
+
+sub dateobsToMJD {
+    my $dateobs = shift;
+
+    my ($date, $time) = split "T", $dateobs;
+
+    my ($year, $mon, $day) = split "-", $date;
+
+    my ($hr, $min, $sec) = split ":",   $time;
+    my $fraction = $sec - int($sec);
+    $sec = int($sec);
+
+    my $ticks = timegm($sec, $min, $hr, $day, $mon-1, $year-1900);
+    $ticks += $fraction;
+
+    # I'm not allowing for leap seconds here but for our purposes here that is ok
+    my $mjd = 40587.0 + ($ticks / 86400.);
+    return $mjd;
+}
+
+sub my_die
+{
+    my $msg = shift;
+    my $fault = shift;
+
+    print STDERR $msg;
+
+    # we don't fault the request here pstamp_parser_run.pl handles that if necessary
+
+    exit $fault;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstampstopfaulted
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstampstopfaulted	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/pstampstopfaulted	(revision 41103)
@@ -0,0 +1,20 @@
+#!/bin/bash
+#
+# script executed by the task pstamp.stopfaulted to stop proceessing of jobs
+# and dependents that have fault_count >= some maximum value. That value is expected
+# to be passed in as an argument otherwise the default in pstamptool is used
+# All arguments are passed to pstamptool
+
+# note: 25 is PSTAMP_NOT_AVAILABLE
+fault_code=25
+
+date
+echo stopping faulted dependent jobs
+cmd="pstamptool -stopdependentjob -set_fault $fault_code $*"
+echo $cmd
+$cmd
+
+echo stopping faulted jobs
+cmd="pstamptool -updatejob -state run -set_state stop -set_fault $fault_code $*"
+echo $cmd
+$cmd
Index: /branches/ccl_branches/ipponly-20191108/pstamp/scripts/request_finish.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/scripts/request_finish.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/scripts/request_finish.pl	(revision 41103)
@@ -0,0 +1,109 @@
+#!/bin/env perl
+
+# request_finish.pl
+
+use warnings;
+use strict;
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+
+use Sys::Hostname;
+use IPC::Cmd 0.36 qw( can_run run );
+use File::Temp qw( tempfile );
+use File::Copy;
+use File::Basename qw( dirname );
+
+use PS::IPP::Metadata::Config;
+use PS::IPP::Metadata::Stats;
+use PS::IPP::Metadata::List qw( parse_md_list );
+
+use PS::IPP::Config qw( :standard );
+use PS::IPP::PStamp::RequestFile qw( :standard );
+
+my ( $req_id, $req_name, $req_file, $req_type, $outdir, $product, $dbname, $dbserver, $verbose, $save_temps, $redirect_output );
+
+my $ipprc = PS::IPP::Config->new();
+
+GetOptions(
+           'req_id=s'   => \$req_id,
+           'req_name=s' => \$req_name,
+           'req_file=s' => \$req_file,
+           'req_type=s' => \$req_type,
+           'outdir=s'  => \$outdir,
+           'product=s'  => \$product,
+	   'dbname=s'   => \$dbname,
+	   'dbserver=s' => \$dbserver,
+	   'verbose'    => \$verbose,
+	   'save-temps' => \$save_temps,
+           'redirect-output' => \$redirect_output,
+) or pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+
+my $err = "";
+
+$err .= "--req_id is required\n" if !$req_id;
+$err .= "--req_type is required\n" if !$req_type;
+$err .= "--req_file is required\n" if !$req_file;
+$err .= "--req_name is required\n" if !$req_name;
+$err .= "--product is required\n" if !$product;
+# $err .= "--outdir is required\n" if !$outdir;
+
+die "$err" if $err;
+
+
+if (!$outdir) {
+    $outdir = dirname($req_file);
+}
+
+if ($redirect_output) {
+    my $logDest = "$outdir/reqfinish.$req_id.log";
+    $ipprc->redirect_output($logDest);
+}
+
+if (!$dbserver) {
+    $dbserver =  metadataLookupStr($ipprc->{_siteConfig}, 'PS_DBSERVER');
+}
+
+my $missing_tools;
+
+my $pstamptool  = can_run('pstamptool')  or (warn "Can't find pstamptool"  and $missing_tools = 1);
+my $pstamp_finish  = can_run('pstamp_finish.pl')  or (warn "Can't find pstamp_finish.pl"  and $missing_tools = 1);
+my $dquery_finish  = can_run('dquery_finish.pl')  or (warn "Can't find dquery_finish.pl"  and $missing_tools = 1);
+
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+my $finish_cmd;
+if ($req_type eq 'pstamp') {
+    $finish_cmd = $pstamp_finish;
+} elsif ($req_type eq 'dquery') {
+    $finish_cmd = $dquery_finish;
+}
+if ($finish_cmd) {
+    my $command = $finish_cmd . " --req_id $req_id --req_name $req_name --req_file $req_file --product $product --outdir $outdir";
+    $command   .= " --dbname $dbname" if $dbname;
+    $command   .= " --dbserver $dbserver" if $dbserver;
+    $command   .= " --verbose" if $verbose;
+    $command   .= " --save-temps" if $save_temps;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        die("Unable to perform $command error code: $error_code");
+    }
+} else {
+    # Unknown request type. Stop job with fault invalid request.
+    print STDERR "request  $req_id has unknown reqType $req_type\n" if $verbose;
+
+    my $command = "$pstamptool -updatereq -req_id $req_id -set_state stop -set_fault $PSTAMP_INVALID_REQUEST";
+    $command   .= " -dbname $dbname" if $dbname;
+    $command   .= " -dbserver $dbserver" if $dbserver;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        die("Unable to perform $command error code: $error_code");
+    }
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/Makefile.am
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/Makefile.am	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/Makefile.am	(revision 41103)
@@ -0,0 +1,56 @@
+bin_PROGRAMS = ppstamp pstampdump
+
+include_HEADERS = \
+	pstamp.h
+
+noinst_HEADERS = \
+	ppstamp.h  \
+	pstampint.h \
+        pstampErrorCodes.h
+
+ppstamp_CPPFLAGS = $(PPSTAMP_CFLAGS)
+pstamprequest_CPPFLAGS = $(PPSTAMP_CFLAGS)
+pstampdump_CPPFLAGS = $(PPSTAMP_CFLAGS)
+
+ppstamp_LDFLAGS = $(PPSTAMP_LIBS)
+pstamprequest_LDFLAGS = $(PPSTAMP_LIBS)
+pstampdump_LDFLAGS = $(PPSTAMP_LIBS)
+
+ppstamp_SOURCES = \
+	ppstamp.c \
+	ppstampArguments.c \
+	ppstampCleanup.c \
+        pstampErrorCodes.c \
+	ppstampMakeStamp.c \
+	ppstampMosaic.c \
+	ppstampOptions.c \
+	ppstampParseCamera.c \
+        ppstampRegion.c \
+	ppstampVersion.c \
+	pstampGetROI.c
+
+pstamprequest_SOURCES = \
+	pstamprequest.c \
+	pstampGetROI.c \
+	ppstampOptions.c
+
+pstampdump_SOURCES = \
+        pstampdump.c
+
+
+clean-local:
+	-rm -f TAGS
+
+# Tags for emacs
+tags:
+	etags `find . -name \*.[ch] -print`
+
+### Error codes.
+BUILT_SOURCES = pstampErrorCodes.h pstampErrorCodes.c
+CLEANFILES = pstampErrorCodes.h pstampErrorCodes.c
+
+pstampErrorCodes.h : pstampErrorCodes.dat pstampErrorCodes.h.in
+	$(ERRORCODES) --data=pstampErrorCodes.dat --outdir=. pstampErrorCodes.h
+
+pstampErrorCodes.c : pstampErrorCodes.dat pstampErrorCodes.c.in pstampErrorCodes.h
+	$(ERRORCODES) --data=pstampErrorCodes.dat --outdir=. pstampErrorCodes.c
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstamp.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstamp.c	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstamp.c	(revision 41103)
@@ -0,0 +1,39 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "ppstamp.h"
+
+int main(int argc, char **argv)
+{
+    ppstampOptions *options = NULL;
+    int exitCode;
+
+    psTimerStart(TIMERNAME);
+    psLibInit(NULL);
+    pstampErrorRegister();
+
+    pmConfig *config = ppstampArguments(argc, argv, &options);
+    if (config == NULL) {
+        psErrorStackPrint(stderr, "Unable to parse command-line arguments.");
+        ppstampCleanup(config, options);
+        exit(PS_EXIT_CONFIG_ERROR);
+    }
+
+    // define the active I/O files
+    if (!ppstampParseCamera(config, options)) {
+        psErrorStackPrint(stderr, "Unable to parse camera.");
+        ppstampCleanup(config, options);
+        exit(PS_EXIT_CONFIG_ERROR);
+    }
+
+    // find the pixels that we need to copy, setup the output image
+    exitCode = ppstampMakeStamp(config, options);
+
+    psLogMsg ("ppstamp", 3, "Complete ppstamp run: %f sec\n", psTimerMark(TIMERNAME));
+
+    // Cleaning up
+    ppstampCleanup(config, options);
+
+    return exitCode;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstamp.h
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstamp.h	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstamp.h	(revision 41103)
@@ -0,0 +1,41 @@
+#ifndef PP_STAMP_H
+#define PP_STAMP_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "pstampint.h"
+#include "ppstampOptions.h"
+
+#define TIMERNAME "ppstamp"             // Name of timer
+
+// Determine the processing options
+bool *ppstampOptionsParse(pmConfig *config);
+
+// Get the configuration
+pmConfig *ppstampArguments(int argc, char **argv, ppstampOptions **);
+
+// Determine what type of camera, and initialise
+bool ppstampParseCamera(pmConfig *config, ppstampOptions *options);
+
+int ppstampMakeStamp(pmConfig *config, ppstampOptions *);
+pmFPAfile * ppstampBuildMosaic(pmConfig *config, pmFPAfile *input, pmFPAview *view);
+
+// free memory, check for leaks
+void ppstampCleanup (pmConfig *config, ppstampOptions *options);
+
+/// Return short version information
+psString ppstampVersion(void);
+
+/// Return long version information
+psString ppstampVersionLong(void);
+
+/// Update the metadata with version information for all dependencies
+void ppstampVersionMetadata(psMetadata *metadata, ppstampOptions *options);
+
+psRegion *ppstampCellRegion(const pmCell *cell);
+psRegion *ppstampChipRegion(const pmChip *chip);
+
+extern bool ppstampMegacamWorkaround;
+#endif
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampArguments.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampArguments.c	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampArguments.c	(revision 41103)
@@ -0,0 +1,190 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+#include <pslib.h>
+#include "ppstamp.h"
+#include "ppstampOptions.h"
+#include <ctype.h>
+
+static void usage (void)
+{
+    fprintf(stderr, "\n");
+
+    fprintf(stderr, "USAGE: ppstamp -skycenter RA DEC -pixrange dx dy    [-file INPUT.fits] [-list INPUT.txt] OUTPUT\n");
+    fprintf(stderr, "       ppstamp -skycenter RA DEC -arcrange dRA dDEC [-file INPUT.fits] [-list INPUT.txt] OUTPUT\n");
+    fprintf(stderr, "       ppstamp -pixcenter x y    -pixrange dx dy    [-class_id class_id] [-file INPUT.fits] [-list INPUT.txt] OUTPUT\n");
+    fprintf(stderr, "       ppstamp -pixcenter x y    -arcrange dRA dDEC [-class_id class_id] [-file INPUT.fits] [-list INPUT.txt] OUTPUT\n");
+    fprintf(stderr, "\n");
+
+    fprintf(stderr, "-skycenter is specified as HH:MM:SS DD:MM:SS or decimal degrees\n");
+    fprintf(stderr, "-arcrange values are seconds of arc\n");
+    fprintf(stderr, "\n");
+    fprintf(stderr, "Optional arguments:\n");
+    fprintf(stderr, "   [-class_id class_id]  : selects class_id (only used with -pixcenter)\n");
+    fprintf(stderr, "   [-astrom astrom.cmp]  : provide an alternative astrometry calibration\n");
+    fprintf(stderr, "   [-mask   mk_image]    : mask image\n");
+    fprintf(stderr, "   [-variance var_image] : variance image\n");
+    fprintf(stderr, "   [-sources sources]    : sources cmf (ignored for chip stage)\n");
+    fprintf(stderr, "   [-forheader mdcfile   : metadata config file contaiing keywords to add to fits headers\n");
+    fprintf(stderr, "   [-stage stage]        : stage of input image (raw, chip, warp, stack, diff)\n");
+    fprintf(stderr, "   [-write_jpeg]         : write a JPEG  format of the image stamp\n");
+    fprintf(stderr, "   [-write_cmf]          : create an output cmf with the sources overlapping the stamp\n");
+    fprintf(stderr, "   [-wholefile]          : ignore the region of interest and process the entire input image\n");
+    fprintf(stderr, "   [-centeroffchip]          : allow center to be off chip boundary and include any pixels in ROI (testing) \n");
+    // fprintf(stderr, "   [-no_censor_masked]   : do not set masked pixels to NAN\n");
+    fprintf(stderr, "\n");
+
+    exit (2);
+}
+
+pmConfig *ppstampArguments(int argc, char **argv, ppstampOptions **pOptions)
+{
+    int argnum;                         // Argument number of interest
+    bool gotCenter = false;
+    bool gotRange = false;
+    ppstampOptions *options;
+
+    if (argc == 1) {
+        usage();
+    }
+
+    if (psArgumentGet (argc, argv, "-version")) {
+        psString version;
+        version = ppstampVersionLong();   fprintf (stdout, "%s\n", version); psFree (version);
+        version = psModulesVersionLong(); fprintf (stdout, "%s\n", version); psFree (version);
+        version = psLibVersionLong();     fprintf (stdout, "%s\n", version); psFree (version);
+        exit (0);
+    }
+
+    // load the site-wide configuration information
+    pmConfig *config = pmConfigRead(&argc, argv, NULL);
+    if (config == NULL) {
+        psError(PSTAMP_ERR_CONFIG, false, "Can't read site configuration!\n");
+        return(NULL);
+    }
+
+    options = ppstampOptionsAlloc();
+    *pOptions = options;
+
+    if ((argnum = psArgumentGet(argc, argv, "-centeroffchip"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        options->centeroffchip = true;
+    }
+	
+    if ((argnum = psArgumentGet(argc, argv, "-wholefile"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        gotCenter = true;
+        gotRange = true;
+        options->wholeFile = true;
+    } else {
+        if (!pstampGetROI(&options->roip, &argc, argv, &gotCenter, &gotRange)) {
+            usage();
+        }
+    }
+
+    if (!gotCenter) {
+        psError(PSTAMP_ERR_ARGUMENTS, true, "must specify center of region of interest\n");
+        usage();
+    }
+
+    if (!gotRange) {
+        psError(PSTAMP_ERR_ARGUMENTS, true, "must specify extent of region of interest\n");
+        usage();
+    }
+
+    if ((argnum = psArgumentGet(argc, argv, "-class_id"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        options->chipName = psStringCopy(argv[argnum]);
+        psArgumentRemove(argnum, &argc, argv);
+    }
+    if ((argnum = psArgumentGet(argc, argv, "-stage"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        options->stage = psStringCopy(argv[argnum]);
+        psArgumentRemove(argnum, &argc, argv);
+    } else {
+        // Should we require this? 
+        options->stage = psStringCopy("unknown");
+    }
+    if ((argnum = psArgumentGet(argc, argv, "-write_jpeg"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        options->writeJPEG = true;
+    }
+    if ((argnum = psArgumentGet(argc, argv, "-write_cmf"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        options->writeCMF = true;
+    }
+
+    // XXX: Note: the various list options have never been tested with ppstamp to my knowledge
+    bool gotAstrom = false;
+    if ((argnum = psArgumentGet(argc, argv, "-astrom"))) {
+        gotAstrom = true;
+        pmConfigFileSetsMD(config->arguments, &argc, argv, "ASTROM", "-astrom", "-astromlist");
+    }
+    pmConfigFileSetsMD(config->arguments, &argc, argv, "MASK",   "-mask", "-masklist");
+    pmConfigFileSetsMD(config->arguments, &argc, argv, "VARIANCE", "-variance", "-variancelist");
+
+    if ((argnum = psArgumentGet(argc, argv, "-sources"))) {
+        pmConfigFileSetsMD(config->arguments, &argc, argv, "SOURCES", "-sources", "-sourceslist");
+        // supplying a sources file implies that we want to save the output
+        options->writeCMF = true;
+    } else if (options->writeCMF && !gotAstrom) {
+        // if we didn't get a sources file but the user wanted us to write the sources we must
+        // have an astrometry file supplied
+        // XXX: Is this too restrictive? Could the sources be contained in say the input file?
+        psError(PSTAMP_ERR_ARGUMENTS, true, "cannot write cmf file unless input sources are supplied\n");
+        usage();
+    }
+
+    // the input image file is a required argument; if not found, we will exit
+    bool status = pmConfigFileSetsMD (config->arguments, &argc, argv, "INPUT", "-file", "-list");
+    if (!status) {
+        psError(PSTAMP_ERR_ARGUMENTS, true, "must specify INPUT\n");
+        usage();
+    }
+
+    if ((argnum = psArgumentGet(argc, argv, "-forheader"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        if (argnum >= argc) {
+            psError(PSTAMP_ERR_ARGUMENTS, true, "metadata config filename value required with option -forheader");
+            usage();
+        }
+
+        const char *filename = argv[argnum];
+        psArgumentRemove(argnum, &argc, argv);
+
+        unsigned int nFail = 0;
+        options->headerAdditions = psMetadataConfigRead(NULL, &nFail, filename, false);
+        if (nFail || !options->headerAdditions) {
+            psError(PS_ERR_UNKNOWN, false, "failed to read metadata from %s", filename);
+            return false;
+        }
+    }
+    
+    if ((argnum = psArgumentGet(argc, argv, "-no_censor_masked"))) {
+        // this is the default. This is for compatiability
+        psArgumentRemove(argnum, &argc, argv);
+        options->censorMasked = false;
+    }
+    if ((argnum = psArgumentGet(argc, argv, "-censor_masked"))) {
+        // default changed to not censor allow it to be changed back to true
+        psArgumentRemove(argnum, &argc, argv);
+        options->censorMasked = true;
+    }
+    if ((argnum = psArgumentGet(argc, argv, "-nocompress"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        options->nocompress = true;
+    }
+
+    // finally the only argument left must be output outroot
+    if (argc < 2) {
+        psError(PSTAMP_ERR_ARGUMENTS, true, "must specify OUTPUT\n");
+        usage();
+    } else if (argc > 2) {
+        psError(PSTAMP_ERR_ARGUMENTS, true, "unknown argument(s)\n");
+        usage();
+    }
+
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "OUTPUT", 0, "Name of the output image", argv[1]);
+
+    return config;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampCleanup.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampCleanup.c	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampCleanup.c	(revision 41103)
@@ -0,0 +1,25 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+# include "ppstamp.h"
+
+void ppstampCleanup (pmConfig *config, ppstampOptions *options)
+{
+    // Free memory used by ppstamp
+    psFree(config);
+    psFree(options);
+
+    // Free memory used by psModules
+    pmConceptsDone();
+    pmConfigDone();
+    pmModelClassCleanup();
+
+    // Free memory used by psLib
+    psLibFinalize();
+
+    // psMemCheckLeaks (0, NULL, stderr, false);
+    // fprintf (stderr, "Found %d leaks in %s\n", psMemCheckLeaks (0, NULL, NULL, false), "ppstamp");
+
+    return;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampMakeStamp.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampMakeStamp.c	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampMakeStamp.c	(revision 41103)
@@ -0,0 +1,908 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+#include <strings.h>
+
+#include "ppstamp.h"
+#include "pmAstrometryUtils.h"
+
+#define WCS_NONLIN_TOL 0.001            // Non-linear tolerance for header WCS
+
+typedef enum {
+    PPSTAMP_OFF,
+    PPSTAMP_PARTIALLY_ON,
+    PPSTAMP_ON,
+    PSTAMP_ERROR
+} ppstampOverlap;
+
+static void skyToChip(pmAstromObj *pt, pmFPA *fpa, pmChip *chip);
+static void chipToSky(pmAstromObj *pt, pmFPA *fpa, pmChip *chip);
+static bool setMaskedToNAN(pmConfig *config, psImage *image, psImage *mask, psImage *variance);
+static bool copySources(pmReadout *outputReadout, pmReadout *inputReadout, pmReadout *astromReadout, psRegion extractRegion);
+static bool imageHasValidPixels(psImage *image);
+
+// convert the input chip's transforms to the output
+static bool convertWCS(pmHDU *outHDU, pmFPA *inFPA, pmChip *inChip, pmFPA *outFPA, pmChip *outChip, psRegion *roi)
+{
+    PS_ASSERT_PTR_NON_NULL(outHDU, 1);
+    PS_ASSERT_PTR_NON_NULL(outHDU->header, 1);
+
+    // Center of stamp in chip coordinates
+    double center_x = (roi->x0 + roi->x1) / 2;
+    double center_y = (roi->y0 + roi->y1) / 2;
+    pmAstromObj *check = pmAstromObjAlloc();
+    check->chip->x = center_x;
+    check->chip->y = center_y;
+    chipToSky(check, inFPA, inChip);
+    double r_before = check->sky->r;
+    double d_before = check->sky->d;
+    psLogMsg("ppstampMakeStamp", 2, "Before fit  Center Pixel RA: %f   DEC: %f (degrees)\n",
+             r_before * PS_DEG_RAD, d_before * PS_DEG_RAD);
+
+    if (outFPA->toSky->type != PS_PROJ_TAN) {
+        // we need to make astrometry terms for the output which are centered on the output chip center,
+        // but we keep the original plate scale
+        psFree(outFPA->toSky);
+        outFPA->toSky = psProjectionAlloc (r_before, d_before, inFPA->toSky->Xs, inFPA->toSky->Ys, PS_PROJ_TAN);
+
+        if (!pmAstromLinearizeToSky(inFPA, inChip, outFPA, outChip, roi)) {
+            psFree(check);
+            psError(PS_ERR_UNKNOWN, false, "Failed to linearize astrometry\n");
+            return false;
+        }
+        check->chip->x = (roi->x1 - roi->x0) / 2;
+        check->chip->y = (roi->y1 - roi->y0) / 2;
+        chipToSky(check, outFPA, outChip);
+        double r_after = check->sky->r;
+        double d_after = check->sky->d;
+
+        psLogMsg("ppstampMakeStamp", 2, "After fit:  Center Pixel RA: %f   DEC: %f (degrees)\n",
+                 r_after * PS_DEG_RAD, d_after * PS_DEG_RAD);
+        psLogMsg("ppstampMakeStamp", 2, "Error in fit to astrometry %.2f    %.2f (arcseconds)\n",
+                 (r_after - r_before) *  PS_DEG_RAD * 3600, (d_after - d_before) * PS_DEG_RAD * 3600);
+
+        // XXX: should we fail if the fit is bad ??
+
+    } else {
+        outChip->toFPA     = psPlaneTransformSetCenter(NULL, inChip->toFPA, (int) roi->x0, (int) roi->y0);
+        outChip->fromFPA   = psPlaneTransformInvert(NULL, outChip->toFPA, *roi, 50);
+        if (!outChip->fromFPA) {
+            psError(PS_ERR_UNKNOWN, false, "inversion of toFPA transform failed");
+            return false;
+        }
+    }
+    psFree(check);
+
+    if (!pmAstromWriteWCS(outHDU->header, outFPA, outChip, WCS_NONLIN_TOL)) {
+        psError(PS_ERR_UNKNOWN, false, "Failed to write WCS to output\n");
+        return false;
+    }
+
+    return true;
+}
+
+static bool copyMetadata(pmFPAfile *output, pmFPAfile *input, pmChip *inChip, ppstampOptions *options, pmAstromObj *center, pmFPAfile *astrom, pmFPAview *inView)
+{
+    pmChip    *outChip;
+    pmFPAview *outView = pmFPAviewAlloc(0);
+
+    // our output file has a single chip
+    outView->chip = 0;
+    outChip = pmFPAviewThisChip(outView, output->fpa);
+    psFree(outView);
+    pmHDU *outHDU = pmHDUGetHighest(output->fpa, outChip, NULL);
+
+    // copy data from the input's chip header to the output.
+    // since some of the keywords might be duplicated we may not want to copy both
+
+    pmHDU *inHDU  = NULL;
+    if (strcmp(options->stage, "raw") && input != astrom) {
+        // Copy the header from the astrometry file since it contains the psphot and psastro results
+        pmChip *astromChip = pmFPAviewThisChip(inView, astrom->fpa);
+        inHDU  = pmHDUFromChip(astromChip);
+    } else {
+        inHDU  = pmHDUFromChip(inChip);
+    }
+
+    if (inHDU->header) {
+        outHDU->header = psMetadataCopy(outHDU->header, inHDU->header);
+    } else if (!outHDU->header) {
+        outHDU->header = psMetadataAlloc();
+    }
+
+    // copy the fpa and chip concepts
+    pmConceptsCopyFPA(output->fpa, input->fpa, false, false);
+    // Needed to preserve FPA.ZP
+    pmConceptsCopyChip(outChip, inChip, false);
+
+    // copy the cell concepts (needed to get CELL.EXPOSURE which gets used to set EXPTIME)
+    pmCell *outCell = outChip->cells->data[0]; // The only output cell
+
+    if (inChip->cells->n == 1) {
+        pmConceptsCopyCell(outCell, inChip->cells->data[0]);
+        // Need to fix up the trimsec and biassec to correspond to the output
+        psMetadataItem *trimsec = psMetadataLookup(outCell->concepts, "CELL.TRIMSEC");
+        psFree(trimsec->data.V);
+        trimsec->data.V = NULL;
+        psMetadataItem *biassec = psMetadataLookup(outCell->concepts, "CELL.BIASSEC");
+        psFree(biassec->data.V);
+        biassec->data.V = psListAlloc(NULL);
+    } else {
+        psList *inCells = psArrayToList(inChip->cells); // Input cells
+        pmConceptsAverageCells(outCell, inCells, NULL, NULL, false);
+        psFree(inCells);
+    }
+
+    // If input had WCS convert it for the new image coordinate system of our stamp
+    if (input->fpa->toSky) {
+        // copy the input fpa's transforms
+        // XXX: if we change to making multiple stamps per process we'll need to copy
+        // the transformations
+        output->fpa->toTPA   = psMemIncrRefCounter(input->fpa->toTPA);
+        output->fpa->fromTPA = psMemIncrRefCounter(input->fpa->fromTPA);
+        output->fpa->toSky   = psMemIncrRefCounter(input->fpa->toSky);
+
+        if (!convertWCS(outHDU, input->fpa, inChip, output->fpa, outChip, &options->roi)) {
+            return false;
+        }
+    } else {
+        psLogMsg("ppstampMakeStamp", PS_LOG_WARN, "No WCS present in input.");
+    }
+
+    // Save our specific metadata
+    psMetadataAddF64(outHDU->header, PS_LIST_TAIL, "RA_DEG", PS_META_REPLACE, "Right Ascension of stamp center", RAD_TO_DEG(center->sky->r));
+    psMetadataAddF64(outHDU->header, PS_LIST_TAIL, "DEC_DEG", PS_META_REPLACE, "Declination of stamp center", RAD_TO_DEG(center->sky->d));
+
+    // XXX: TODO: save the region of interest boundary and the offset to be applied. 
+    // XXX: Gene thinks that there may be a way to handle this in the WCS
+
+    // Now a few sanity checks
+    // Make sure zero point in the fpa concepts matches the observed value in the input header
+    bool status;
+    float zpt_obs = psMetadataLookupF32(&status, outHDU->header, "ZPT_OBS");
+    if (status) {
+        psMetadataAddF32(output->fpa->concepts, PS_LIST_TAIL, "FPA.ZP", PS_META_REPLACE, "Magnitude zero point", zpt_obs);
+    }
+
+    ppstampVersionMetadata(outHDU->header, options);
+
+    // copy any user supplied keywords
+    if (options->headerAdditions) {
+        if (!psMetadataOverlay(outHDU->header, options->headerAdditions)) {
+            psError(PS_ERR_UNKNOWN, false, "Failed to copy header additions to output\n");
+            return false;
+        }
+    }
+
+
+    return true;
+}
+
+// Extract pixels from an image. If part of the bounds of the region are
+// partially outside of the input those pixels are set to zero.
+static psImage *extractStamp(psImage *image, psRegion region, double value)
+{
+    int width  = region.x1 - region.x0 + 0.5;
+    int height = region.y1 - region.y0 + 0.5;
+
+    if (width < 0) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "negative width\n");
+        return NULL;
+    }
+    if (height < 0) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "negative height\n");
+        return NULL;
+    }
+
+    int srcX  = region.x0;
+    int srcY  = region.y0;
+    int lastX = region.x1;
+    int lastY = region.y1;
+    if (lastX > (image->col0 + image->numCols)) {
+        lastX = image->col0 + image->numCols;
+    }
+    if (lastY > (image->row0 + image->numRows)) {
+        lastY = image->row0 + image->numRows;
+    }
+
+    int leftBlank = 0;
+    int dstX  = 0;
+    if (srcX < image->col0) {
+        leftBlank = image->col0 - srcX;
+        dstX += leftBlank;
+        srcX = 0;
+    }
+
+    int copyWidth  = lastX - srcX;
+    if (copyWidth <= 0) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "copyWidth is invalid: %d\n", copyWidth);
+        return NULL;
+    }
+    if (copyWidth > width) {
+        copyWidth = width;
+    }
+
+    psImage *output = psImageAlloc(width, height, image->type.type);
+    if (!output) {
+        psError(PS_ERR_UNKNOWN, false, "psImageAlloc failed\n");
+        return NULL;
+    }
+
+    if (!psImageInit(output, value)) {
+        psError(PS_ERR_UNKNOWN, false, "psInitImage failed\n");
+        return NULL;
+    }
+
+    psElemType  type = image->type.type;
+    psS32       elementSize =  PSELEMTYPE_SIZEOF(type);
+    int         copySize = copyWidth * elementSize;
+
+    int     dstY = 0;
+    psS32   skip = image->row0 - srcY;
+    if (skip > 0) {
+        dstY = skip;
+        srcY = image->row0;
+    }
+    for ( ; dstY < height; srcY++, dstY++) {
+        if (srcY >= lastY) {
+            break;
+        }
+        // copy copyWidth pixels from srcX,srcY to dstX, dstY
+        psU8 *pdst = output->data.U8[dstY] + (dstX * elementSize);
+
+        psU8 *psrc = image->data.U8[srcY]  + (srcX * elementSize);
+
+        memcpy(pdst, psrc, copySize);
+    }
+
+    return output;
+}
+
+// Build the postage stamp output file
+
+static int makeStamp(pmConfig *config, ppstampOptions *options, pmFPAfile *input,
+                pmChip *inChip, pmFPAview *view, pmAstromObj *center, pmFPAfile *astrom)
+{
+    int status = false;
+
+    pmFPAfile *output = psMetadataLookupPtr(NULL, config->files, options->outputFileRule);
+    if (!output) {
+        psError(PS_ERR_UNKNOWN, false, "Can't find output data\n");
+        return PS_EXIT_DATA_ERROR;
+    }
+    pmFPAview *outview = pmFPAviewAlloc(0);
+    pmFPAAddSourceFromView(output->fpa, outview, output->format);
+
+    outview->chip = 0;
+    outview->cell = 0;
+    pmCell *target =  pmFPAviewThisCell(outview, output->fpa); // Target cell
+    psFree(outview);
+    outview = NULL;
+
+    //   psMetadataPrint(stderr, inChip->concepts, 0);
+
+    // These default to zero. would that be ok?
+    psMetadataAddS32(target->concepts, PS_LIST_TAIL, "CELL.XBIN", PS_META_REPLACE, "Binning in x", 1);
+    psMetadataAddS32(target->concepts, PS_LIST_TAIL, "CELL.YBIN", PS_META_REPLACE, "Binning in y", 1);
+
+    pmFPAfile *srcFile;
+    pmFPAview *srcView = pmFPAviewAlloc(0);
+    if (psArrayLength(inChip->cells) > 1) {
+        // we extract our postage stamp from a mosaic
+        pmFPAfile *mosaic = ppstampBuildMosaic(config, input, view);
+        if (mosaic == NULL) {
+            return PS_EXIT_UNKNOWN_ERROR;
+        }
+        srcFile = mosaic;
+        srcView->chip = 0;
+    } else {
+        srcFile = input;
+        *srcView = *view;
+    }
+
+    if (astrom->camera != srcFile->camera) {
+        psError(PS_ERR_UNKNOWN, true, "Input camera and astrometry camera do not match");
+        return PS_EXIT_CONFIG_ERROR;
+    }
+
+    // At this point we know we have one cell
+    srcView->cell = 0;
+
+    pmReadout *readout;
+    while ((readout = pmFPAviewNextReadout (srcView, srcFile->fpa, 1)) != NULL) {
+        if (!readout->data_exists) {
+            psError(PS_ERR_UNKNOWN, false, "no data in input readout!\n");
+            continue;
+        }
+        pmReadout *outReadout = pmReadoutAlloc(target);
+        if (!outReadout) {
+            psError(PS_ERR_UNKNOWN, false, "failed to allocate output readout\n");
+            status = false;
+            break;
+        }
+
+        psRegion extractRegion = options->roi;
+
+        // Close your eyes while I hack around bug 986
+        if (ppstampMegacamWorkaround) {
+            // the coordinates of the mosaic are shifted 32 pixels from the chip
+            // TODO does this always apply? For example I doubt that applies to
+            // skycells.
+            extractRegion.x0 -= 32;
+            extractRegion.x1 -= 32;
+        }
+
+        outReadout->image = extractStamp(readout->image, extractRegion,  NAN);
+        if (!outReadout->image) {
+            psError(PS_ERR_UNKNOWN, false, "failed to create postage stamp image\n");
+            status = false;
+            break;
+        }
+        if (!imageHasValidPixels(outReadout->image)) {
+            return PSTAMP_NO_VALID_PIXELS;
+        }
+        if (readout->variance) {
+            outReadout->variance = extractStamp(readout->variance, extractRegion,  NAN);
+            if (!outReadout->variance) {
+                psError(PS_ERR_UNKNOWN, false, "failed to create postage stamp weight image\n");
+                status = false;
+                break;
+            }
+        }
+        if (readout->mask) {
+            psImageMaskType maskInitValue = pmConfigMaskGet("BLANK", config);
+            outReadout->mask = extractStamp(readout->mask, extractRegion,  maskInitValue);
+            if (!outReadout->mask) {
+                psError(PS_ERR_UNKNOWN, false, "failed to create postage stamp mask image\n");
+                status = false;
+                break;
+            }
+
+            if (options->censorMasked && !setMaskedToNAN(config, outReadout->image, outReadout->mask, outReadout->variance)) {
+                psError(PS_ERR_UNKNOWN, false, "failed to create postage stamp mask image\n");
+                status = false;
+                break;
+            }
+        }
+
+        outReadout->data_exists = true;
+        outReadout->parent->data_exists = true;
+        outReadout->parent->parent->data_exists = true;
+        status = true;
+
+        if (options->writeCMF) {
+            pmReadout *sourcesReadout = NULL;
+            if (astrom->fpa != input->fpa) {
+                sourcesReadout = pmFPAviewThisReadout(srcView, astrom->fpa);
+            } else {
+                sourcesReadout = readout;
+            }
+            if (!copySources(outReadout, sourcesReadout, sourcesReadout, extractRegion)) {
+                psError(PS_ERR_UNKNOWN, false, "failed to extract sources from region of interest.\n");
+                status = false;
+                break;
+            }
+        }
+
+        psFree(outReadout); // drop reference
+    }
+
+    psFree(srcView);
+
+    if (status) {
+        // For raw stage we need to do more work to build a proper header
+        status = copyMetadata(output, input, inChip, options, center, astrom, view);
+    }
+    return status ? PS_EXIT_SUCCESS : PS_EXIT_UNKNOWN_ERROR;
+}
+
+
+
+static bool regionContainsPoint(psRegion *r, psPlane *pt)
+{
+    if (pt->x < r->x0)
+        return false;
+    if (pt->x >= r->x1)
+        return false;
+    if (pt->y < r->y0)
+        return false;
+    if (pt->y >= r->y1)
+        return false;
+
+    return true;
+}
+
+// true if the inner region is equal to or completely contained in
+// the outer region
+static bool regionContainsRegion(psRegion *outer, psRegion *inner)
+{
+    if ((outer->x0 <= inner->x0) &&
+        (outer->y0 <= inner->y0) &&
+        (outer->x1 >= inner->x1) &&
+        (outer->y1 >= inner->y1)) {
+        return true;
+    } else {
+        return false;
+    }
+}
+
+
+static void TPToChip(pmAstromObj *pt, pmFPA *fpa, pmChip *chip)
+{
+    psPlaneTransformApply(pt->FP, fpa->fromTPA, pt->TP);
+    // convert from FP to chip
+    psPlaneTransformApply(pt->chip, chip->fromFPA, pt->FP);
+}
+static void skyToChip(pmAstromObj *pt, pmFPA *fpa, pmChip *chip)
+{
+    // convert from sky to TP to FP
+    psProject(pt->TP, pt->sky, fpa->toSky);
+    TPToChip(pt, fpa, chip);
+}
+
+static void chipToSky(pmAstromObj *pt, pmFPA *fpa, pmChip *chip)
+{
+    // chip to FP
+    psPlaneTransformApply(pt->FP, chip->toFPA, pt->chip);
+    // FP to TP to sky
+    psPlaneTransformApply(pt->TP, fpa->toTPA, pt->FP);
+    psDeproject(pt->sky, pt->TP, fpa->toSky);
+}
+
+static void compareToBox(psRegion *region, psPlane *pt)
+{
+    if (pt->x < region->x0)
+        region->x0 = pt->x;
+    if (pt->x > region->x1)
+        region->x1 = pt->x;
+    if (pt->y < region->y0)
+        region->y0 = pt->y;
+    if (pt->y > region->y1)
+        region->y1 = pt->y;
+}
+
+// For roi width and height given in sky coordinates find a bounding box in chip coordinates
+// that encloses the requested range
+static void findBoundingBox(ppstampOptions *options, pmFPA *fpa, pmChip *chip, pmAstromObj *center)
+{
+    pmAstromObj *pt = pmAstromObjAlloc();
+
+    // calculate the four corners of the bounding box in sky coordinates, translate them to
+    // chip coordinates and build the ROI by comparison.
+
+    options->roi.x0 = INFINITY;
+    options->roi.x1 = -INFINITY;
+    options->roi.y0 = INFINITY;
+    options->roi.y1 = -INFINITY;
+
+    double dx = 0.5 * options->roip.dRA  / fpa->toSky->Xs;
+    double dy = 0.5 * options->roip.dDEC / fpa->toSky->Ys;
+
+    // XXX: why do we limit this?
+    if (dx > 8000) {
+        psWarning( "requested width %f too large reducing to 8000\n", dx);
+        dx = 8000;
+    }
+    if (dy > 8000) {
+        psWarning( "requested height %f too large reducing to 8000\n", dy);
+        dy = 8000;
+    }
+
+    pt->TP->xErr = 0;
+    pt->TP->yErr = 0;
+
+    pt->TP->x = center->TP->x - dx;
+    pt->TP->y = center->TP->y - dy;
+    TPToChip(pt, fpa, chip);
+    compareToBox(&options->roi, pt->chip);
+
+    pt->TP->x = center->TP->x + dx;
+    pt->TP->y = center->TP->y - dy;
+    TPToChip(pt, fpa, chip);
+    compareToBox(&options->roi, pt->chip);
+
+    pt->TP->x = center->TP->x + dx;
+    pt->TP->y = center->TP->y + dy;
+    TPToChip(pt, fpa, chip);
+    compareToBox(&options->roi, pt->chip);
+
+    pt->TP->x = center->TP->x - dx;
+    pt->TP->y = center->TP->y + dy;
+    TPToChip(pt, fpa, chip);
+    compareToBox(&options->roi, pt->chip);
+
+    psFree(pt);
+
+    // save the width and height in case we need them later
+    options->roip.dX = options->roi.x1 - options->roi.x0;
+    options->roip.dY = options->roi.y1 - options->roi.y0;
+}
+
+// findROI
+// calculate the region of interest in chip coordinates and determine whether that region
+// is completely contained on a single chip
+
+static ppstampOverlap findROI(ppstampOptions *options, pmFPAview *view,
+                    pmFPAfile *input, pmFPAfile *astrom, bool bilevelAstrometry,
+                    pmChip *chip, pmAstromObj *center)
+{
+    psString chipName = psMetadataLookupStr(NULL, chip->concepts, "CHIP.NAME");
+    psRegion    *chipBounds = ppstampChipRegion(chip);
+    bool        onChip = false;
+    ppstampOverlap   returnval = PPSTAMP_OFF;
+
+    // set up the astrometry
+    pmHDU *hdu = pmFPAviewThisHDU(view, astrom->fpa);
+    PS_ASSERT_PTR_NON_NULL(hdu, 1)
+    PS_ASSERT_PTR_NON_NULL(hdu->header, 1)
+
+    // we can live without astrometry if the ROI is completely specified in pixel coordinates
+    bool mustHaveAstrometry = false;
+    if (options->roip.celestialCenter || options->roip.celestialRange) {
+        mustHaveAstrometry = true;
+    }
+    if (bilevelAstrometry) {
+        if (!pmAstromReadBilevelChip (chip, hdu->header) && mustHaveAstrometry) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to read bilevel chip astrometry for input FPA.");
+            return false;
+        }
+    } else {
+        if (!pmAstromReadWCS (input->fpa, chip, hdu->header, 1.0) && mustHaveAstrometry) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to read WCS astrometry for input FPA.");
+            return false;
+        }
+    }
+
+    if (options->roip.celestialCenter) {
+
+        center->sky->r = options->roip.centerRA;
+        center->sky->d = options->roip.centerDEC;
+        center->sky->rErr = 0;
+        center->sky->dErr = 0;
+
+        skyToChip(center, input->fpa, chip);
+
+        if (regionContainsPoint(chipBounds, center->chip)) {
+            psLogMsg("ppstampMakeStamp", 2, "Found center (%f %f) on chip: %s\n",
+                center->chip->x, center->chip->y, chipName);
+            onChip = true;
+        }
+    } else {
+        // center specified in pixels.
+        // If the user specified a name of a chip name wait until we get to that one.
+        // If no chip name was specified, select this one (the first one that had data)
+        if ((options->chipName == NULL) || !strcasecmp(chipName, options->chipName)) {
+            if (options->chipName) {
+                psLogMsg("ppstampMakeStamp", 2, "Center on chip: %s\n", chipName);
+            }
+            if (options->wholeFile) {
+                center->chip->x = (chipBounds->x1 + chipBounds->x0 + 1) / 2;
+                center->chip->y = (chipBounds->y1 + chipBounds->y0 + 1) / 2;
+            } else {
+                center->chip->x = options->roip.centerX;
+                center->chip->y = options->roip.centerY;
+            }
+            center->chip->xErr = 0;
+            center->chip->yErr = 0;
+            onChip = true;
+            if (input->fpa->toSky) {
+                chipToSky(center, input->fpa, chip);
+            }
+        }
+    }
+
+    //MEH add hack for ROI not to include center on chip
+    if (onChip || options->centeroffchip ) {
+        if (options->roip.celestialRange) {
+            findBoundingBox(options, input->fpa, chip, center);
+        } else {
+            if (options->wholeFile) { 
+                options->roi = *chipBounds;
+            } else {
+                int width  = options->roip.dX;
+                int height = options->roip.dY;
+                if (width > 8000) {
+                    psWarning( "requested width %d too large reducing to 8000\n", width);
+                    width = 8000;
+                }
+                if (height > 8000) {
+                    psWarning( "requested height %d too large reducing to 8000\n", height);
+                    height = 8000;
+                }
+
+                // calculate the ROI in chip coordinates
+                options->roi.x0 = center->chip->x - width / 2;
+                options->roi.x1 = options->roi.x0 + width;
+                options->roi.y0 = center->chip->y - height / 2;
+                options->roi.y1 = options->roi.y0 + height;
+            }
+        }
+
+
+        if (options->wholeFile || regionContainsRegion(chipBounds, &options->roi)) {
+            psLogMsg("ppstampMakeStamp", 2, "ROI contained on: %s\n", chipName);
+            returnval = PPSTAMP_ON;
+        } else {
+            psLogMsg("ppstampMakeStamp", 2, "Partial Overlap chip %s\n", chipName);
+            psLogMsg("ppstampMakeStamp", 2, "ROI:         %s\n", psRegionToString(options->roi));
+            psLogMsg("ppstampMakeStamp", 2, "Chip Extent: %s\n", psRegionToString(*chipBounds));
+
+            returnval = PPSTAMP_PARTIALLY_ON;
+        }
+    }
+    psFree(chipBounds);
+
+    return returnval;
+}
+
+int ppstampMakeStamp (pmConfig *config, ppstampOptions *options)
+{
+    bool        status = false;
+    int        returnval = PS_EXIT_SUCCESS;;
+    bool        foundOverlap = false;
+
+    pmFPAfile *input = psMetadataLookupPtr(&status, config->files, "PPSTAMP.INPUT");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "Can't find input file!\n");
+        return PS_EXIT_DATA_ERROR;
+    }
+
+    pmFPAfile *astrom = psMetadataLookupPtr(NULL, config->files, "PSWARP.ASTROM");
+    if (!astrom) {
+        astrom = input;
+    }
+
+    pmFPAview *view = pmFPAviewAlloc(0);// View for level of interest
+
+    // files associated with the science image
+    if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
+        psError(PS_ERR_UNKNOWN, false, "Failed to load input.");
+        psFree (view);
+        return PS_EXIT_DATA_ERROR;
+    }
+    bool bilevelAstrometry  = false;
+    pmHDU *phu = pmFPAviewThisPHU(view, astrom->fpa);
+    if (phu) {
+        char *ctype = psMetadataLookupStr(NULL, phu->header, "CTYPE1");
+        if (ctype) {
+            bilevelAstrometry = !strcmp (&ctype[4], "-DIS");
+        }
+    }
+    if (bilevelAstrometry) {
+        if (!pmAstromReadBilevelMosaic(input->fpa, phu->header)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to read bilevel mosaic astrometry for input FPA.");
+            psFree(view);
+            return PS_EXIT_DATA_ERROR;
+        }
+    }
+
+    // Loop over the chips and find the one that contains the center
+    pmAstromObj *center = pmAstromObjAlloc();
+    pmChip *chip;
+    while ((chip = pmFPAviewNextChip(view, input->fpa, 1)) != NULL) {
+        bool allDone = false;
+
+        if (!chip->process || !chip->file_exists) {
+            continue;
+        }
+
+        if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to load chip");
+            status = false;
+            break;
+        }
+
+        ppstampOverlap overlap = findROI(options, view, input, astrom, bilevelAstrometry, chip, center);
+
+        switch (overlap) {
+        case PPSTAMP_OFF:
+            // keep looking
+            break;
+        case PPSTAMP_ON:
+        case PPSTAMP_PARTIALLY_ON:
+            returnval = makeStamp(config, options, input, chip, view, center, astrom);
+            allDone = true;
+            foundOverlap = true;
+            break;
+        case PSTAMP_ERROR:
+            returnval = PS_EXIT_UNKNOWN_ERROR;
+            allDone = true;
+            break;
+        default:
+            psError(PS_ERR_PROGRAMMING, false, "findROI returned unexpected value %d\n", overlap);
+            break;
+        }
+
+        pmFPAfileIOChecks(config, view, PM_FPA_AFTER);
+
+        if (allDone) {
+            view->chip = -1;
+            break;
+        }
+    }
+    pmFPAfileIOChecks(config, view, PM_FPA_AFTER);
+
+    psFree(center);
+    psFree(view);
+
+    if (!foundOverlap && (returnval == PS_EXIT_SUCCESS)) {
+        psWarning( "ROI not found in input\n");
+        returnval = PSTAMP_NO_OVERLAP;
+    }
+
+    return returnval;
+}
+
+
+static bool setMaskedToNAN(pmConfig *config, psImage *image, psImage *mask, psImage *variance)
+{
+#ifdef notdef
+    bool status;
+    psMetadata *masks = psMetadataLookupMetadata(&status, config->recipes, "MASKS");
+    if (!status) {
+        psError(PM_ERR_CONFIG, false, "failed to lookup MASKS in recipes\n");
+        return false;
+    }
+    // we set anything masked to NAN except if CONV.POOR is the only bit set
+    // First check the old value
+    psU32 convPoor = psMetadataLookupU32(&status, masks, "POOR.WARP");
+    if (!status) {
+        convPoor = psMetadataLookupU32(&status, masks, "CONV.POOR");
+        if (!status) {
+            psError(PM_ERR_CONFIG, false, "failed to lookup mask value for CONV.POOR in recipes\n");
+            return false;
+        }
+    }
+    psU32 suspect = psMetadataLookupU32(&status, masks, "SUSPECT");
+    if (!status) {
+        psError(PM_ERR_CONFIG, false, "failed to lookup mask value for SUSPECT in recipes\n");
+        return false;
+    }
+    psU32 maskMask = ~(convPoor | suspect);
+
+    double exciseValue;
+    if (image->type.type == PS_TYPE_U16) {
+        exciseValue = 0xffff;
+    } else if (image->type.type == PS_TYPE_F32) {
+        exciseValue = NAN;
+    } else {
+         psError(PS_ERR_PROGRAMMING, true, "unexpected image type: %d\n", image->type.type);
+        return false;
+    }
+    long numExcised = 0;
+    for (int y=0; y<image->numRows; y++) {
+        for (int x=0; x<image->numCols; x++) {
+            psU16 maskVal = psImageGet(mask, x, y);
+            if (maskVal & maskMask) {
+                numExcised++;
+                psImageSet(image, x, y, exciseValue);
+                if (variance) {
+                    psImageSet(variance, x, y, exciseValue);
+                }
+            }
+        }
+    }
+#endif
+
+    long numCensored;
+    if (!pmCensorMasked(config, image, mask, variance, &numCensored)) {
+        psError(PS_ERR_UNKNOWN, false, "Failed to censor masked pixels.\n");
+        return false;
+    }
+
+    psLogMsg("ppstamp", PS_LOG_DETAIL, "Censored %ld masked pixels\n", numCensored);
+
+    return true;
+}
+
+static bool copySources(pmReadout *outReadout, pmReadout *inReadout, pmReadout *astromReadout, psRegion extractRegion) {
+    bool status;
+
+    // first look for detections in the input readout. Those would have come from the -sources file
+    if (!inReadout->analysis) {
+        psLogMsg("ppstampMakeStamp", PS_LOG_WARN, "no analysis metadata found on input\n");
+        return false;
+    }
+    pmDetections *inDetections = psMetadataLookupPtr (&status, inReadout->analysis, "PSPHOT.DETECTIONS");
+    if (!inDetections) {
+        // No detections on the input readout. Try the astrometry readout if supplied.
+        if (astromReadout) {
+            inDetections = psMetadataLookupPtr (&status, astromReadout->analysis, "PSPHOT.DETECTIONS");
+            if (!inDetections) {
+                psLogMsg("ppstampMakeStamp", PS_LOG_WARN, "no detections found on input or astrometry readout\n");
+                return false;
+            } else {
+                psLogMsg("ppstampMakeStamp", PS_LOG_INFO, "using detections from astrom file\n");
+            }
+        } else {
+            psLogMsg("ppstampMakeStamp", PS_LOG_WARN, "no detections found on input readout\n");
+            return false;
+        }
+    }
+    if (!inDetections->allSources) {
+        psLogMsg("ppstampMakeStamp", PS_LOG_WARN, "no sources array found on detections structure\n");
+        return false;
+    }
+
+    // Ok we have sources, allocate a detections structure and copy them to the output readout
+    psArray *inSources = inDetections->allSources;
+
+    // copy analysis metadata in order to pick up any data stored there from the input file.
+    // This includes detection efficiency analysis and extended source parameters.
+    psMetadataCopy(outReadout->analysis, inReadout->analysis);
+
+    // now replace the sources with a new container
+    pmDetections *outDetections = pmDetectionsAlloc();
+    psMetadataAddPtr (outReadout->analysis, PS_LIST_TAIL, "PSPHOT.DETECTIONS", PS_META_REPLACE | PS_DATA_UNKNOWN,
+        "psphot detections", outDetections);
+
+    psArray *outSources = outDetections->allSources = psArrayAllocEmpty( 100 );
+
+    // include sources that are off the stamp by just a little bit 
+    // XXX: should I be doing this?
+    #define EXPAND_SIZE 16
+    psRegion sourceRegion = extractRegion;
+    sourceRegion.x0 -= EXPAND_SIZE;
+    sourceRegion.y0 -= EXPAND_SIZE;
+    sourceRegion.x1 += EXPAND_SIZE;
+    sourceRegion.y1 += EXPAND_SIZE;
+
+    bool adjustCoords = extractRegion.x0 != 0 || extractRegion.y0 != 0;
+
+    for (int i = 0; i < inSources->n; i++ ) {
+        pmSource *src = inSources->data[i];
+
+        float x = src->peak->xf;
+        float y = src->peak->yf;
+        if (x < sourceRegion.x0 || x > sourceRegion.x1 ||
+            y < sourceRegion.y0 || y > sourceRegion.y1) {
+            continue;
+        }
+        if (adjustCoords) {
+            // adjust the source position for the stamp coordinate system
+            src->peak->x  -= extractRegion.x0;
+            src->peak->xf -= extractRegion.x0;
+
+            src->peak->y  -= extractRegion.y0;
+            src->peak->yf -= extractRegion.y0;
+            if (src->moments) {
+                src->moments->Mx -= extractRegion.x0;
+                src->moments->My -= extractRegion.y0;
+            }
+            if (src->modelPSF) {
+                src->modelPSF->params->data.F32[PM_PAR_XPOS] -= extractRegion.x0;
+                src->modelPSF->params->data.F32[PM_PAR_YPOS] -= extractRegion.y0;
+            }
+
+            if (src->modelFits) {
+                for (int j = 0; j < src->modelFits->n; j++) {
+                    pmModel *model = src->modelFits->data[j];
+                    assert (model != NULL);
+                    model->params->data.F32[PM_PAR_XPOS] -= extractRegion.x0;
+                    model->params->data.F32[PM_PAR_YPOS] -= extractRegion.y0;
+                }
+            }
+        }
+
+        psArrayAdd(outSources, 100, src);
+    }
+
+    return true;
+}
+
+static bool
+imageHasValidPixels(psImage *image) {
+    // check F32 image and return true if any pixel is finite
+    if (image->type.type != PS_TYPE_F32) {
+        return true;
+    }
+    for (int y=0; y<image->numRows; y++) {
+        for (int x=0; x<image->numCols; x++) {
+            psF32 pixel = image->data.F32[y][x];
+            if (isfinite(pixel)) {
+                return true;
+            }
+        }
+    }
+    return false;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampMosaic.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampMosaic.c	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampMosaic.c	(revision 41103)
@@ -0,0 +1,40 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "ppstamp.h"
+
+// Mosaic the cells of a given chip
+
+
+pmFPAfile *ppstampBuildMosaic(pmConfig *config, pmFPAfile *input, pmFPAview *view)
+{
+    bool    status;
+
+    pmFPAfile *mosaic =  psMetadataLookupPtr(&status, config->files, "PPSTAMP.CHIP");
+    if (!status)  {
+        psErrorStackPrint(stderr, "can't find mosaic i/o file\n");
+        exit(EXIT_FAILURE);
+    }
+
+    pmFPAview *mosaicView = pmFPAviewAlloc(0);
+
+    mosaicView->chip = 0;
+    pmChip  *mChip  = pmFPAviewThisChip(mosaicView, mosaic->fpa);
+    pmChip  *inChip = pmFPAviewThisChip(view, input->fpa);
+    if (!mChip->hdu && !mChip->parent->hdu) {
+        pmFPAAddSourceFromView(mosaic->fpa, mosaicView, mosaic->format);
+    }
+    psFree(mosaicView);
+
+    psMaskType blankMask = pmConfigMaskGet("BLANK", config);
+
+    status = pmChipMosaic(mChip, inChip, true, blankMask);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false, "chip mosaic failed\n");
+        return NULL;
+    }
+
+    return mosaic;
+}
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampOptions.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampOptions.c	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampOptions.c	(revision 41103)
@@ -0,0 +1,42 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pslib.h>
+#include "ppstampOptions.h"
+
+
+static void pstampOptionsFree(ppstampOptions *options)
+{
+    psFree(options->chipName);
+    psFree(options->headerAdditions);
+}
+
+ppstampOptions *ppstampOptionsAlloc(void)
+{
+    ppstampOptions *options = psAlloc(sizeof(ppstampOptions));
+    psMemSetDeallocator(options, (psFreeFunc)pstampOptionsFree);
+
+    options->roip.celestialCenter = false;
+    options->roip.centerX         = 0;
+    options->roip.centerY         = 0;
+    options->roip.centerRA        = 0;
+    options->roip.centerDEC       = 0;
+    options->roip.celestialRange  = false;
+    options->roip.dX   = 0;
+    options->roip.dY   = 0;
+    options->roip.dRA  = 0;
+    options->roip.dDEC = 0;
+    options->chipName  = NULL;
+    options->cellName  = NULL;
+    options->stage  = NULL;
+    options->headerAdditions = NULL;
+    options->censorMasked = false;
+    options->writeJPEG = false;
+    options->writeCMF = false;
+    options->nocompress = false;
+    options->wholeFile = false;
+    options->outputFileRule = NULL;
+
+    return options;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampOptions.h
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampOptions.h	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampOptions.h	(revision 41103)
@@ -0,0 +1,31 @@
+#ifndef PPSTAMP_OPTIONS_H
+#define PPSTAMP_OPTIONS_H
+
+#include "pstampROI.h"
+
+// Options for ppstamp processing
+typedef struct {
+    // input arguments
+    pstampROI   roip;
+    bool    	centeroffchip;
+    bool        wholeFile;
+    psString    chipName;
+    psString    cellName;
+    psString    stage;
+    psMetadata  *headerAdditions;
+    bool        censorMasked;
+    bool        writeJPEG;
+    bool        writeCMF;
+    bool        nocompress;
+    //
+    // Calculated Values
+    //
+    psString    outputFileRule;
+    psRegion    roi;            // roi in chip coordinates
+} ppstampOptions;
+
+ppstampOptions *ppstampOptionsAlloc(void);
+
+#endif
+
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampParseCamera.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampParseCamera.c	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampParseCamera.c	(revision 41103)
@@ -0,0 +1,177 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+# include "ppstamp.h"
+
+bool ppstampMegacamWorkaround = false;
+
+// Set up the ppstamp output Image file
+bool setupOutput(pmConfig *config, pmFPAfile *input, ppstampOptions *options, bool doMask, bool doWeight, pmFPAfile **pOutput)
+{
+    if (!options->stage || (strcmp(options->stage, "diff") != 0)) {
+        if (options->nocompress) {
+            options->outputFileRule = "PPSTAMP.OUTPUT.NOCOMP";
+        } else {
+            options->outputFileRule = "PPSTAMP.OUTPUT";
+        }
+    } else {
+        // need special filerule for diff stage image to allow for negative values
+        if (options->nocompress) {
+            options->outputFileRule = "PPSTAMP.OUTPUT.DIFF.NOCOMP";
+        } else {
+            options->outputFileRule = "PPSTAMP.OUTPUT.DIFF";
+        }
+    }
+    pmFPAfile *output = pmFPAfileDefineSkycell(config, NULL, options->outputFileRule);
+    if (!output) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to setup output.");
+        return false;
+    }
+    *pOutput = output;
+
+    output->save = true;
+
+    if (doMask) {
+        char *rule;
+        if (options->nocompress) {
+            rule =  "PPSTAMP.OUTPUT.MASK.NOCOMP";
+        } else {
+            rule =  "PPSTAMP.OUTPUT.MASK";
+        }
+        pmFPAfile *outMask = pmFPAfileDefineSkycell(config, output->fpa, rule);
+        outMask->save = true;
+    }
+    if (doWeight) {
+        char *rule;
+        if (options->nocompress) {
+            rule =  "PPSTAMP.OUTPUT.VARIANCE.NOCOMP";
+        } else {
+            rule =  "PPSTAMP.OUTPUT.VARIANCE";
+        }
+        pmFPAfile *outWeight = pmFPAfileDefineSkycell(config, output->fpa, rule);
+        outWeight->save = true;
+    }
+
+    return true;
+}
+
+// This function seems mis-named. What are we doing with regards to the Camera?
+// Well we are building the output image based on the camera. Is there
+// something else that I'm missing?
+
+bool ppstampParseCamera(pmConfig *config, ppstampOptions *options)
+{
+    bool status = false;
+
+    // the input image defines the camera, and all recipes and options the follow
+    pmFPAfile *input = pmFPAfileDefineFromArgs (&status, config, "PPSTAMP.INPUT", "INPUT");
+    if (!status || !input) {
+        psError(PS_ERR_IO, false, "Failed to build FPA from PPSTAMP.INPUT");
+        return false;
+    }
+    if (input->type != PM_FPA_FILE_IMAGE) {
+        psError(PS_ERR_IO, true, "PPSTAMP.INPUT is not of type IMAGE");
+        return false;
+    }
+
+    bool doMask = false;
+    pmFPAfile *mask = pmFPAfileBindFromArgs (&status, input, config, "PPSTAMP.INPUT.MASK", "MASK");
+    if (!status) {
+        psError(PS_ERR_IO, false, "Failed to build FPA from PPSTAMP.INPUT.MASK");
+        return false;
+    }
+    if (mask) {
+        doMask = true;
+    }
+    bool doWeight = false;
+    pmFPAfile *weight = pmFPAfileBindFromArgs (&status, input, config, "PPSTAMP.INPUT.VARIANCE", "VARIANCE");
+    if (!status) {
+        psError(PS_ERR_IO, false, "Failed to build FPA from PPSTAMP.INPUT.VARIANCE");
+        return false;
+    }
+    if (weight) {
+        doWeight = true;
+    }
+        
+    pmFPAfile *astrom = pmFPAfileDefineFromArgs(&status, config, "PSWARP.ASTROM", "ASTROM");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false, "failed to find astrom definitiion");
+        return NULL;
+    }
+    if (astrom) {
+        psLogMsg ("ppstamp", 3, "Using supplied astrometry.\n");
+    } else {
+        psLogMsg ("ppstamp", 3, "Using header astrometry.\n");
+    }
+
+    // Set up the output target
+    pmFPAfile *output;
+    if (!setupOutput(config, input, options, doMask, doWeight, &output)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to setup output.");
+        return false;
+    }
+
+    // Set up the input and output sources files if needed
+    if (options->writeCMF) {
+        bool status;
+
+        // see if -sources file was supplied. 
+        // If so define the file.
+        psPtr sourcesFile = psMetadataLookupPtr(&status, config->arguments, "SOURCES");
+        if (sourcesFile) {
+            pmFPAfile *sources = pmFPAfileBindFromArgs(&status, input, config, "PPSTAMP.INPUT.SOURCES", "SOURCES");
+            if (!status) {
+                psError(psErrorCodeLast(), false, "Failed to load file definition for PPSTAMP.INPUT.SOURCES");
+                return false;
+            }
+            if (!sources && !astrom) {
+                psError(psErrorCodeLast(), false, "Failed to define input sources file");
+                return false;
+            }
+        } else {
+            psLogMsg ("ppstamp", PS_LOG_INFO, "Output sources file requested but no -sources supplied.\n");
+            psLogMsg ("ppstamp", PS_LOG_INFO, "Will use the sources in the supplied astrometry file.\n");
+        }
+        
+        pmFPAfile *outputSources = NULL;
+        outputSources = pmFPAfileDefineSkycell(config, output->fpa, "PPSTAMP.OUTPUT.SOURCES");
+        if (!outputSources) {
+            psError(psErrorCodeLast(), false, "Failed to define output sources file");
+            return false;
+        }
+        outputSources->save = true;
+    }
+
+    pmFPAfile *chipImage = pmFPAfileDefineChipMosaic(config, input->fpa, "PPSTAMP.CHIP");
+    if (!chipImage) {
+        psError(PS_ERR_IO, false, _("Unable to generate new file from PPSTAMP.CHIP"));
+        return NULL;
+    }
+    if (chipImage->type != PM_FPA_FILE_IMAGE) {
+        psError(PS_ERR_IO, true, "PPSTAMP.CHIP is not of type IMAGE");
+        return NULL;
+    }
+
+    if (options->writeJPEG) {
+        char *filerule = "PPSTAMP.OUTPUT.JPEG";
+        pmFPAfile *jpg = pmFPAfileDefineSkycell(config, output->fpa, filerule);
+        if (!jpg) {
+            psError(PS_ERR_IO, false, "Unable to generate new file from %s", filerule);
+            return NULL;
+        }
+        if (jpg->type != PM_FPA_FILE_JPEG) {
+            psError(PS_ERR_IO, true, "%s is not of type JPEG", filerule);
+            return NULL;
+        }
+        jpg->save = true;
+    }
+
+    // XXX: TODO: only do the workaround if the file level is chip, skycells should be fine
+    if (!strcmp(config->cameraName, "MEGACAM")) {
+        // workaround bug 986 and related
+        ppstampMegacamWorkaround = true;
+    }
+ 
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampRegion.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampRegion.c	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampRegion.c	(revision 41103)
@@ -0,0 +1,121 @@
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "ppstamp.h"
+
+// Functions to calculate the image space boundaries of a given Chip or Cell
+// These are calculated in coordinates that match the wcs transformation
+// (adapted from pmFPAExtent.c)
+
+// return cell pixels bounding the readout
+static psRegion *ppstampReadoutRegion(const pmReadout *readout)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, NULL);
+
+    psImage *image = readout->image;    // Image from which to get dimensions
+    if (!image) {
+        return NULL;
+    }
+
+    // This is the difference between this function and pmReadoutExtent. 
+    // pmReadoutExtent ignores the col0, row0 of the readout
+   
+    int col0 = 0;   // should be  image->col0 - readout->col0
+    int row0 = 0;   //            image->row0 - readout->row0
+   
+    return psRegionAlloc(col0, col0 + image->numCols,
+                         row0, row0 + image->numRows);
+}
+
+// return chip pixels bounding the cell (all readouts)
+psRegion *ppstampCellRegion(const pmCell *cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+
+    psArray *readouts = cell->readouts; // Array of component readouts
+    psRegion *cellExtent = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of cell
+    for (long i = 0; i < readouts->n; i++) {
+        pmReadout *readout = readouts->data[i]; // Readout of interest
+        psRegion *roExtent = ppstampReadoutRegion(readout); // Extent of readout
+        cellExtent->x0 = PS_MIN(cellExtent->x0, roExtent->x0);
+        cellExtent->x1 = PS_MAX(cellExtent->x1, roExtent->x1);
+        cellExtent->y0 = PS_MIN(cellExtent->y0, roExtent->y0);
+        cellExtent->y1 = PS_MAX(cellExtent->y1, roExtent->y1);
+        psFree(roExtent);
+    }
+
+    bool mdok;                          // Status of MD lookup
+    int cellX0 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.X0"); // Cell x offset
+    if (!mdok) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find CELL.X0.\n");
+        psFree(cellExtent);
+        return NULL;
+    }
+
+    int cellY0 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.Y0"); // Cell y offset
+    if (!mdok) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find CELL.Y0.\n");
+        psFree(cellExtent);
+        return NULL;
+    }
+    int xParity = psMetadataLookupS32(NULL, cell->concepts, "CELL.XPARITY");
+    int yParity = psMetadataLookupS32(NULL, cell->concepts, "CELL.YPARITY");
+
+    if (xParity >= 0) {
+        cellExtent->x0 += cellX0;
+        cellExtent->x1 += cellX0;
+    } else {
+        float x0 = cellX0 - cellExtent->x1;
+        float x1 = cellX0 - cellExtent->x0;
+        cellExtent->x0 = x0;
+        cellExtent->x1 = x1;
+    }
+
+    if (yParity >= 0) {
+        cellExtent->y0 += cellY0;
+        cellExtent->y1 += cellY0;
+    } else {
+        float y0 = cellY0 - cellExtent->y1;
+        float y1 = cellY0 - cellExtent->y0;
+        cellExtent->y0 = y0;
+        cellExtent->y1 = y1;
+    }
+
+    return cellExtent;
+}
+
+// return chip pixels included in all cells
+psRegion *ppstampChipRegion(const pmChip *chip)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+
+    if (ppstampMegacamWorkaround) {
+        // There is an inconsistency in the megacam parameters. 
+        // The offset to the cells is effectively contained in two
+        // places.
+        // The value of CELL.X0 for the 2 cells is 0 and 1024 respectivly
+        // while the two values for readout->image.col0 = 32 and 1056
+        // This fact makes it impossible to calculate the Chip bounds 
+        // in a way consistent with say gpc1
+        // I'm deferring this problem for now.
+        // Since all chips on megacam have the same bounds, I just hard code
+        // the answer. See bug 986
+        return psRegionAlloc(32, 2080, 0, 4612);
+    }
+
+    psArray *cells = chip->cells;       // Array of component cells
+    psRegion *chipExtent = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of chip
+    for (long i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // Cell of interest
+        psRegion *cellExtent = ppstampCellRegion(cell); // Extent of cell
+        chipExtent->x0 = PS_MIN(chipExtent->x0, cellExtent->x0);
+        chipExtent->x1 = PS_MAX(chipExtent->x1, cellExtent->x1);
+        chipExtent->y0 = PS_MIN(chipExtent->y0, cellExtent->y0);
+        chipExtent->y1 = PS_MAX(chipExtent->y1, cellExtent->y1);
+        psFree(cellExtent);
+    }
+
+    return chipExtent;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampVersion.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampVersion.c	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/ppstampVersion.c	(revision 41103)
@@ -0,0 +1,56 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "ppstamp.h"
+
+static const char *cvsTag = "$Name: not supported by cvs2svn $";// CVS tag name
+
+psString ppstampVersion(void)
+{
+    psString version = NULL;            // Version, to return
+    psStringAppend(&version, "%s-%s",PACKAGE_NAME,PACKAGE_VERSION);
+    return version;
+}
+
+psString ppstampVersionLong(void)
+{
+    psString version = ppstampVersion(); // Version, to return
+    psString tag = psStringStripCVS(cvsTag, "Name"); // CVS tag
+    psStringAppend(&version, " (cvs tag %s) %s, %s", tag, __DATE__, __TIME__);
+    psFree(tag);
+    return version;
+}
+
+
+void ppstampVersionMetadata(psMetadata *metadata, ppstampOptions *options)
+{
+    PS_ASSERT_METADATA_NON_NULL(metadata,);
+
+    psString pslib = psLibVersionLong();// psLib version
+    psString psmodules = psModulesVersionLong(); // psModules version
+    psString ppstamp = ppstampVersionLong(); // ppstamp version
+
+    psTime *time = psTimeGetNow(PS_TIME_TAI); // The time now
+    psString timeString = psTimeToISO(time); // The time in an ISO string
+    psFree(time);
+    psString head = NULL;               // Head string
+    psStringAppend(&head, "ppstamp processing at %s. Component information:", timeString);
+    psFree(timeString);
+
+    psMetadataAddStr(metadata, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, head, "");
+    psMetadataAddStr(metadata, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, pslib, "");
+    psMetadataAddStr(metadata, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, psmodules, "");
+    psMetadataAddStr(metadata, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, ppstamp, "");
+    psString roi = NULL;
+    psStringAppend(&roi, "ppstamp: region of interest: %s", psRegionToString(options->roi));
+    psMetadataAddStr(metadata, PS_LIST_TAIL, "HISTORY",  PS_META_DUPLICATE_OK, roi, "");
+
+    psFree(roi);
+    psFree(head);
+    psFree(pslib);
+    psFree(psmodules);
+    psFree(ppstamp);
+
+    return;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/pstamp.h
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/pstamp.h	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/pstamp.h	(revision 41103)
@@ -0,0 +1,66 @@
+#ifndef PSTAMP_H
+#define PSTAMP_H
+
+// error codes returned to users in results flie
+// PS-IPP-PStamp::RequestFile
+// These must match the values in the perl module PS-IPP-PStamp::RequestFile
+// i.e. PS-IPP-PStamp/lib/PS/IPP/PStamp/RequestFile.pm
+
+typedef enum {
+        PSTAMP_SUCCESS          = 0,
+	PSTAMP_FIRST_ERROR_CODE = 10,
+	PSTAMP_SYSTEM_ERROR     = 10,
+	PSTAMP_NOT_IMPLEMENTED  = 11,
+	PSTAMP_UNKNOWN_ERROR    = 12,
+	PSTAMP_DUP_REQUEST      = 20,
+	PSTAMP_INVALID_REQUEST  = 21,
+	PSTAMP_UNKNOWN_PROJECT  = 22,
+	PSTAMP_NO_IMAGE_MATCH   = 23,
+	PSTAMP_NOT_DESTREAKED   = 24,
+	PSTAMP_NOT_AVAILABLE    = 25,
+	PSTAMP_GONE             = 26,
+	PSTAMP_NO_JOBS_QUEUED   = 27,
+        PSTAMP_NO_OVERLAP       = 28,
+        PSTAMP_NOT_AUTHORIZED   = 29,
+        PSTAMP_NO_VALID_PIXELS  = 30,
+        PSTAMP_BG_RESTORE_NOT_AVAILABLE = 31,
+} pstampJobErrors;
+
+
+// values for options mask.
+#define PSTAMP_SELECT_IMAGE         1
+#define PSTAMP_SELECT_MASK          2
+#define PSTAMP_SELECT_WEIGHT        4
+#define PSTAMP_SELECT_SOURCES       8
+#define PSTAMP_SELECT_CMF           8
+#define PSTAMP_SELECT_PSF           16
+#define PSTAMP_SELECT_BACKMDL       32
+#define PSTAMP_SELECT_JPEG          64
+#define PSTAMP_SELECT_EXP           128
+#define PSTAMP_SELECT_NUM           256
+#define PSTAMP_SELECT_UNCOMPRESSED  512
+#define PSTAMP_SELECT_INVERSE       1024
+#define PSTAMP_SELECT_UNCONV        2048
+#define PSTAMP_RESTORE_BACKGROUND   4096
+// MEH -- previously unused                           8192
+#define PSTAMP_MULTI_OVERLAP_IMAGE  8192
+#define PSTAMP_USE_IMFILE_ID        16384
+
+#define PSTAMP_NO_WAIT_FOR_UPDATE   32768
+#ifdef notdef
+#define PSTAMP_REQUEST_UNCENSORED  0x10000
+#define PSTAMP_REQUIRE_UNCENSORED  0x20000
+#endif
+
+#define PSTAMP_CENTER_IN_PIXELS 1
+#define PSTAMP_RANGE_IN_PIXELS  2
+
+#define STAMP_REQUEST_EXTNAME "PS1_PS_REQUEST"
+#define STAMP_REQUEST_VERSION "1"
+
+#define STAMP_RESULTS_EXTNAME "PS1_PS_RESULTS"
+#define STAMP_RESULTS_VERSION "1"
+
+// end of values tha must match PS-IPP-PStamp::RequestFile
+
+#endif
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampErrorCodes.c.in
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampErrorCodes.c.in	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampErrorCodes.c.in	(revision 41103)
@@ -0,0 +1,25 @@
+/*
+ * The line
+    { PSTAMP_ERR_$X{ErrorCode}, "$X{ErrorDescription}"},
+ * (without the Xs)
+ * will be replaced by values from errorCodes.dat
+ */
+#include "pslib.h"
+#include "pstampErrorCodes.h"
+
+void pstampErrorRegister(void)
+{
+    static psErrorDescription errors[] = {
+       { PSTAMP_ERR_BASE, "First value we use; lower values belong to psLib" },
+       { PSTAMP_ERR_${ErrorCode}, "${ErrorDescription}"},
+    };
+    static int nerror = PSTAMP_ERR_NERROR - PSTAMP_ERR_BASE; // number of values in enum
+
+    for (int i = 0; i < nerror; i++) {
+       psErrorDescription *tmp = psAlloc(sizeof(psErrorDescription));
+       *tmp = errors[i];
+       psErrorRegister(tmp, 1);
+       psFree(tmp);			/* it's on the internal list */
+    }
+    nerror = 0;			                // don't register more than once
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampErrorCodes.dat
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampErrorCodes.dat	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampErrorCodes.dat	(revision 41103)
@@ -0,0 +1,10 @@
+#
+# This file is used to generate pstampErrorCodes.h
+#
+BASE = 10000		First value we use; lower values belong to psLib
+UNKNOWN			Unknown pstamp error code
+NOT_IMPLEMENTED		Desired feature is not yet implemented
+ARGUMENTS		Incorrect arguments
+CONFIG			Problem in configure files
+IO			Problem in FITS I/O
+DATA                    Problem in data values
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampErrorCodes.h.in
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampErrorCodes.h.in	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampErrorCodes.h.in	(revision 41103)
@@ -0,0 +1,18 @@
+#if !defined(PSTAMP_ERROR_CODES_H)
+#define PSTAMP_ERROR_CODES_H
+/*
+ * The line
+ *  PSTAMP_ERR_$X{ErrorCode},
+ * (without the X)
+ *
+ * will be replaced by values from errorCodes.dat
+ */
+typedef enum {
+    PSTAMP_ERR_BASE = 10000,
+    PSTAMP_ERR_${ErrorCode},
+    PSTAMP_ERR_NERROR
+} pstampErrorCode;
+
+void pstampErrorRegister(void);
+
+#endif
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampGetROI.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampGetROI.c	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampGetROI.c	(revision 41103)
@@ -0,0 +1,190 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "pstampint.h"
+#include "pstampROI.h"
+#include "ohana.h"
+
+static bool get2Angles(int argnum, int *pArgc, char **argv, bool bothDegrees, bool makePositive, double *p1, double *p2);
+static bool get2Ints(int argnum, int *pArgc, char **argv, bool makePositive, int *p1, int *p2);
+
+
+bool pstampGetROI(pstampROI *roip, int *pArgc, char **argv, bool *gotCenter, bool *gotRange)
+{
+    int argnum;                         // Argument number of interest
+
+    if ((argnum = psArgumentGet(*pArgc, argv, "-pixcenter"))) {
+        *gotCenter = true;
+        roip->celestialCenter = false;
+        psArgumentRemove(argnum, pArgc, argv);
+
+        roip->center[0] = argv[argnum];
+        roip->center[1] = argv[argnum+1];
+
+        if (!get2Ints(argnum, pArgc, argv, false, &roip->centerX, &roip->centerY)) {
+            psError(PSTAMP_ERR_ARGUMENTS, true, "invalid pixcenter specification: %s %s\n",
+                argv[argnum], argv[argnum+1]);
+            return false;
+        }
+        psArgumentRemove(argnum, pArgc, argv);
+        psArgumentRemove(argnum, pArgc, argv);
+    }
+
+    if ((argnum = psArgumentGet(*pArgc, argv, "-skycenter"))) {
+        if (*gotCenter) {
+            psError(PSTAMP_ERR_ARGUMENTS, true, "can't specify both -pixcenter and -skycenter\n");;
+            return false;
+        }
+        psArgumentRemove(argnum, pArgc, argv);
+
+        roip->center[0] = argv[argnum];
+        roip->center[1] = argv[argnum+1];
+
+        double raDeg, decDeg;
+        if (!get2Angles(argnum, pArgc, argv, false, false, &raDeg, &decDeg)) {
+            psError(PSTAMP_ERR_ARGUMENTS, true, "invalid skycenter specification: %s %s\n",
+                argv[argnum], argv[argnum+1]);
+            return false;
+        }
+        psArgumentRemove(argnum, pArgc, argv);
+        psArgumentRemove(argnum, pArgc, argv);
+        roip->centerRA  = DEG_TO_RAD(raDeg);
+        roip->centerDEC = DEG_TO_RAD(decDeg);
+        *gotCenter = true;
+        roip->celestialCenter = true;
+    }
+
+    if ((argnum = psArgumentGet(*pArgc, argv, "-pixrange"))) {
+        *gotRange = true;
+        roip->celestialRange = false;
+        psArgumentRemove(argnum, pArgc, argv);
+        roip->range[0] = argv[argnum];
+        roip->range[1] = argv[argnum+1];
+
+        if (!get2Ints(argnum, pArgc, argv, true, &roip->dX, &roip->dY)) {
+            psError(PSTAMP_ERR_ARGUMENTS, true, "invalid pixrange specification: %s %s\n",
+                argv[argnum], argv[argnum+1]);
+            return false;
+        }
+        psArgumentRemove(argnum, pArgc, argv);
+        psArgumentRemove(argnum, pArgc, argv);
+    }
+
+    if ((argnum = psArgumentGet(*pArgc, argv, "-arcrange"))) {
+        if (*gotRange) {
+            psError(PSTAMP_ERR_ARGUMENTS, true, "specify only one of -pixrange or -arcrange\n");;
+            return false;
+        }
+        psArgumentRemove(argnum, pArgc, argv);
+        *gotRange = true;
+        roip->celestialRange = true;
+
+        roip->range[0] = argv[argnum];
+        roip->range[1] = argv[argnum+1];
+
+        // arcrange values are seconds of arc
+        roip->dRA  = SEC_TO_RAD(fabs(atof(argv[argnum])));
+        roip->dDEC = SEC_TO_RAD(fabs(atof(argv[argnum+1])));
+
+        psArgumentRemove(argnum, pArgc, argv);
+        psArgumentRemove(argnum, pArgc, argv);
+    }
+    // I'm leaving in the -celrange option (HH:MM:SS DD:MM:SS), but not publicizing it
+    if ((argnum = psArgumentGet(*pArgc, argv, "-celrange"))) {
+        if (*gotRange) {
+            psError(PSTAMP_ERR_ARGUMENTS, true, "specify one of -pixrange, -arcrange, or -celrange\n");;
+            return false;
+        }
+        psArgumentRemove(argnum, pArgc, argv);
+        *gotRange = true;
+        roip->celestialRange = true;
+
+        roip->range[0] = argv[argnum];
+        roip->range[1] = argv[argnum+1];
+
+        double deg1, deg2;
+        if (!get2Angles(argnum, pArgc, argv, false, true, &deg1, &deg2)) {
+            psError(PSTAMP_ERR_ARGUMENTS, true, "invalid celrange specification: %s %s\n",
+                argv[argnum], argv[argnum+1]);
+            return false;
+        }
+        psArgumentRemove(argnum, pArgc, argv);
+        psArgumentRemove(argnum, pArgc, argv);
+        roip->dRA = DEG_TO_RAD(deg1);
+        roip->dDEC = DEG_TO_RAD(deg2);
+    }
+
+    if (!*gotCenter) {
+        psError(PSTAMP_ERR_ARGUMENTS, true, "must specify center");
+        return false;
+    }
+    if (!*gotRange) {
+        psError(PSTAMP_ERR_ARGUMENTS, true, "must specify range for stamp");
+        return false;
+    }
+
+    return true;
+}
+
+static bool validNumber(char *string)
+{
+    char *p = string;
+
+    if ((*p == '+') || (*p == '-')) {
+        p++;
+    }
+    return isdigit(*p);
+}
+
+static bool get2Ints(int argnum, int *pArgc, char **argv, bool makePositive, int *p1, int *p2)
+{
+    if (*pArgc < 2) {
+        return false;
+    }
+
+    if (!validNumber(argv[argnum])) {
+        return false;
+    }
+    *p1 = atoi(argv[argnum]);
+
+    if (!validNumber(argv[argnum+1])) {
+        return false;
+    }
+    *p2 = atoi(argv[argnum+1]);
+
+    if (makePositive) {
+        *p1 = abs(*p1);
+        *p2 = abs(*p2);
+    }
+
+    return true;
+}
+
+static bool get2Angles(int argnum, int *pArgc, char **argv, bool bothInDegrees, bool makePositive,
+    double *p1, double *p2)
+{
+    bool rval;
+
+    if (*pArgc < 2) {
+        return false;
+    }
+
+    if (bothInDegrees) {
+        // both values are angles of arc DD:MM:SS or decimal degrees
+        rval   = ohana_dms_to_ddd(p1, argv[argnum]);
+        if (rval) {
+            rval  = ohana_dms_to_ddd(p2, argv[argnum+1]);
+        }
+    } else {
+        // first value may be in HH:MM:SS
+        rval = ohana_str_to_radec(p1, p2, argv[argnum], argv[argnum+1]);
+    }
+
+    if (rval && makePositive) {
+        *p1 = abs(*p1);
+        *p2 = abs(*p2);
+    }
+
+    return rval;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampROI.h
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampROI.h	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampROI.h	(revision 41103)
@@ -0,0 +1,23 @@
+#ifndef PSTAMP_ROI_H
+#define PSTAMP_ROI_H
+
+// Struct defining the Postage Stamp Region of Interest
+
+typedef struct {
+    double centerRA;
+    double centerDEC;
+    int    centerX;
+    int    centerY;
+    double dRA;
+    double dDEC;
+    int    dX;
+    int    dY;
+    bool   celestialCenter;       // true if center is in RA/dec
+    bool   celestialRange;        // true if range is in RA/dec
+    char   *center[2];            // copy of command line strings for center (from argv don't free)
+    char   *range[2];             // copy of command line strings for range (from argv don't free)
+} pstampROI;
+
+bool pstampGetROI(pstampROI *roip, int *pArgc, char **argv, bool *gotCenter, bool *gotRange);
+
+#endif
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampdump.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampdump.c	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampdump.c	(revision 41103)
@@ -0,0 +1,181 @@
+// pstampdump  - read a fits file  containing a postage stamp request or response table and
+//               print the contents to stdout
+//
+//              Actually this should work fine for dumping the rows of any of any kind of fits table
+//              The -header option is pstamp table specific
+
+#include <pslib.h>
+#include <psmodules.h>
+#include <string.h>
+
+static bool readFitsFile(psString fileName, psMetadata **pHeader, psArray **pTable)
+{
+    psFits *fits = psFitsOpen(fileName, "r");
+    if (fits == NULL) {
+        psError(PS_ERR_IO, false, "failed to open %s for output", fileName);
+        return false;
+    }
+    if (!psFitsMoveExtNum(fits, 1, true)) {
+        psError(PS_ERR_IO, false, "failed to move to first extension from %s", fileName);
+        return false;
+    }
+
+    if (pHeader) {
+        *pHeader = psFitsReadHeader(NULL, fits);
+        if (!*pHeader) {
+            psError(PS_ERR_IO, false, "failed to header from %s", fileName);
+            return false;
+        }
+    }
+
+    if (pTable) {
+        *pTable = psFitsReadTable(fits);
+        if (*pTable == NULL) {
+            psError(PS_ERR_IO, false, "psFitsReadTable failed for %s", fileName);
+            return false;
+        }
+    }
+
+    psFitsClose(fits);
+
+    return true;
+}
+
+static void
+usage(char *program_name)
+{
+    fprintf(stderr, "usage: %s [-header] [-simple] filename\n", program_name);
+    exit(1);
+}
+
+int main(int argc, char *argv[])
+{
+    bool dumpHeader = false;
+    bool simple = false;
+    bool dumpTable = true;
+    int argnum;
+    if ((argnum = psArgumentGet(argc, argv, "-header"))) {
+        dumpHeader = true;
+        psArgumentRemove(argnum, &argc, argv);
+    }
+    if ((argnum = psArgumentGet(argc, argv, "-simple"))) {
+        simple = true;
+        psArgumentRemove(argnum, &argc, argv);
+    }
+    if ((argnum = psArgumentGet(argc, argv, "-headeronly"))) {
+        dumpTable = false;
+        dumpHeader = true;
+        psArgumentRemove(argnum, &argc, argv);
+    }
+
+    if (argc != 2) {
+        usage(argv[0]);
+    }
+
+    psString fileName = argv[1];
+
+    psMetadata *header;
+    psArray *array;
+    if (!readFitsFile(fileName, dumpHeader ? &header : NULL, dumpTable ? &array : NULL)) {
+        psErrorStackPrint(stderr, "failed to process fits table from: %s\n", fileName);
+        return PS_EXIT_DATA_ERROR;
+    }
+    if (dumpHeader) {
+        psString extname = psMetadataLookupStr(NULL, header, "EXTNAME");
+        if (!extname) {
+            psErrorStackPrint(stderr, "failed to find EXTNAME in fits header of: %s\n", fileName);
+            return PS_EXIT_DATA_ERROR;
+        }
+        psString req_name = psMetadataLookupStr(NULL, header, "REQ_NAME");
+        if (!req_name) {
+            psErrorStackPrint(stderr, "failed to find REQ_NAME in fits header of: %s\n", fileName);
+            return PS_EXIT_DATA_ERROR;
+        }
+        if (!strcmp(extname, "PS1_PS_REQUEST")) {
+            psString extver = psMetadataLookupStr(NULL, header, "EXTVER");
+            psString action = psMetadataLookupStr(NULL, header, "ACTION");
+            psString email = psMetadataLookupStr(NULL, header, "EMAIL");
+            if (!extver) {
+                // work around bug in MOPS request files
+                // Accept an integer for the version number
+                psS32 extver_num = psMetadataLookupS32(NULL, header, "EXTVER");
+                if (extver_num) {
+                    psStringAppend(&extver, "%d", extver_num);
+                } else {
+                    psErrorStackPrint(stderr, "failed to find EXTVER in fits header of: %s\n", fileName);
+                    return PS_EXIT_DATA_ERROR;
+                }
+            }
+            if (!strcmp(extver, "1")) {
+                printf("%s %s %s\n", extname, extver, req_name);
+            } else {
+                if (!action) {
+                    psErrorStackPrint(stderr, "failed to find action in fits header of: %s\n", fileName);
+                    return PS_EXIT_DATA_ERROR;
+                }
+                if (!email) {
+                    psErrorStackPrint(stderr, "failed to find action in fits header of: %s\n", email);
+                    return PS_EXIT_DATA_ERROR;
+                }
+                printf("%s %s %s %s %s\n", extname, extver, req_name, action, email);
+            }
+        } else if (!strcmp(extname, "PS1_PS_RESULTS")) {
+            psS64 req_id = psMetadataLookupS64(NULL, header, "REQ_ID");
+            printf("%s %s %" PRId64 "\n", extname, req_name, req_id);
+        } else {
+            psErrorStackPrint(stderr, "do not recognize extname: %s in %s\n", extname, fileName);
+            return PS_EXIT_DATA_ERROR;
+        }
+    }
+    if (!dumpTable) {
+        // done
+        return 0;
+    }
+
+    if (!psArrayLength(array)) {
+        fprintf(stderr, "%s contains an empty table\n", fileName);
+        return 0;
+    }
+
+    for (int i=0; i<psArrayLength(array); i++) {
+        psString str = psMetadataConfigFormat(array->data[i]);
+        if (!str) {
+            psErrorStackPrint(stderr, "failed to format metadata item\n");
+            return (PS_EXIT_SYS_ERROR);
+        }
+        if (!simple) {
+            printf("ROW_%d METADATA\n", i+1);
+            printf("%s", str);
+            printf("END\n");
+        } else {
+            // simple output format space separated values one line per row
+            char *p = str;
+            char *pnl;
+            while ((pnl = strchr(p, '\n'))) {
+                // terminate the string for this line
+                *pnl = 0;
+                bool blank = (p == pnl);
+                if (blank) {
+                    p = pnl + 1;
+                    continue;
+                }
+                // split line into space separated tokens
+                char *name = strtok(p, " ");
+                char *type = strtok(NULL, " ");
+                char *val = strtok(NULL, " ");
+
+                // avoid unused variables warning/error
+                (void) name; (void) type;
+
+                if (val) {
+                    printf("%s ", val);
+                }
+                // next line
+                p = pnl + 1;
+            }
+            printf("\n");
+        }
+    }
+
+    return 0;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampint.h
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampint.h	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/pstampint.h	(revision 41103)
@@ -0,0 +1,31 @@
+#ifndef PSTAMP_INT_H
+#define PSTAMP_INT_H
+
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>  // for strcasecmp
+#include <unistd.h>   // for unlink
+#include "pslib.h"
+#include "psmodules.h"
+#include "pstampErrorCodes.h"
+
+#include "pstamp.h"
+typedef enum {
+    PSTAMP_UNKNOWN = -1,
+    PSTAMP_RAW,
+    PSTAMP_CHIP,
+    PSTAMP_WARP,
+    PSTAMP_DIFF,
+    PSTAMP_STACK
+} pstampImageType;
+
+
+// command modes for pstampparse 
+typedef enum {
+    PSP_MODE_UNKNOWN = 0,
+    PSP_MODE_QUEUE_JOB,
+    PSP_MODE_LIST_URI,
+    PSP_MODE_LIST_JOB
+} pspMode;
+
+#endif
Index: /branches/ccl_branches/ipponly-20191108/pstamp/src/pstamprequest.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/src/pstamprequest.c	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/src/pstamprequest.c	(revision 41103)
@@ -0,0 +1,440 @@
+#include <pslib.h>
+#include <string.h>
+#include "pstampint.h"
+#include "pstampROI.h"
+
+// pstamprequest  - create a fits table containing a postage stamp server request
+
+
+typedef struct {
+    psMetadata  *md;
+    psString    fileName;
+    psString    requestName;
+    bool        verbose;
+} psrOptions;
+
+char *string_columns[] = {
+    "PROJECT",
+    "JOB_TYPE",
+    "REQ_TYPE", // byid,  byexp, bydiff, bycoord
+    "ID",       // db id, exposure name, diff_image_id, n/a
+    "COMPONENT",
+    "REQFILT",
+    "STAMP_NAME",
+    NULL
+};
+char *double_columns[]= {
+    "CENTER_X",
+    "CENTER_Y",
+    "WIDTH",
+    "HEIGHT",
+    "MJD_MIN",
+    "MJD_MAX",
+};
+char *u32_columns[] = {
+    "ROWNUM",
+    "COORD_MASK",
+    "OPTION_MASK", // bitmask or of PSTAMP_SELECT_IMAGE PSTAMP_SELECT_MASK PSTAMP_SELECT_WEIGHT
+    NULL
+};
+
+static void usage(int exitStatus)
+{
+    psErrorStackPrint(stderr, "Unable to parse command-line arguments.");
+    exit(exitStatus);
+}
+
+static psMetadata *initializeTable()
+{
+    psMetadata *md = psMetadataAlloc();
+
+    //  unset columns with string type default to "null"
+    for (char **col_name = string_columns; *col_name != NULL; col_name++) {
+        psMetadataAddStr(md, PS_LIST_TAIL, *col_name, PS_META_DEFAULT, "", "null");
+    }
+    //  unset columns with u32 type default to 0
+    for (char **col_name = u32_columns; *col_name != NULL; col_name++) {
+        psMetadataAddU32(md, PS_LIST_TAIL, *col_name, PS_META_DEFAULT, "", 0);
+    }
+    //  unset columns with f64 type default to 0.
+    for (char **col_name = double_columns; *col_name != NULL; col_name++) {
+        psMetadataAddF64(md, PS_LIST_TAIL, *col_name, PS_META_DEFAULT, "", 0.0);
+    }
+    return md;
+}
+static void getId(char *idString, int argnum, int *pArgc, char *argv[], psrOptions *options)
+{
+    if (*pArgc < 2) {
+        fprintf(stderr, "must specify %s\n", idString);
+        usage(PS_EXIT_DATA_ERROR);
+    }
+    // catch common error
+    if (*argv[argnum] == '-') {
+        fprintf(stderr, "%s is not a valid %s\n", argv[argnum], idString);
+        usage(PS_EXIT_DATA_ERROR);
+    }
+
+    psMetadataAddStr (options->md, PS_LIST_TAIL, idString, PS_META_REPLACE, "", argv[argnum]);
+    psArgumentRemove(argnum, pArgc, argv);
+}
+
+
+static pstampImageType getType(int argnum, int *pArgc, char *argv[], psrOptions *options, char *paramName)
+{
+    if (paramName) {
+        if (*pArgc < (argnum+2)) {
+            fprintf(stderr, "must specify image type and %s\n", paramName);
+            usage(PS_EXIT_DATA_ERROR);
+        }
+    }  else {
+        if (*pArgc < (argnum+1)) {
+            fprintf(stderr, "must specify image type");
+            usage(PS_EXIT_DATA_ERROR);
+        }
+    }
+    char *type = argv[argnum];
+
+    psArgumentRemove(argnum, pArgc, argv);
+
+    pstampImageType itype = PSTAMP_UNKNOWN;
+
+    if (strcmp(type, "raw") == 0) {
+        itype = PSTAMP_RAW;
+    } else if (strcmp(type, "chip") == 0) {
+        itype = PSTAMP_CHIP;
+    } else if (strcmp(type, "warp") == 0) {
+        itype = PSTAMP_WARP;
+    } else if (strcmp(type, "diff") == 0) {
+        itype = PSTAMP_DIFF;
+    } else if (strcmp(type, "stack") == 0) {
+        itype = PSTAMP_STACK;
+    } else {
+        fprintf(stderr, "unknown image type %s\n", type);
+        usage(PS_EXIT_DATA_ERROR);
+    }
+    psMetadataAddStr (options->md, PS_LIST_TAIL, "IMG_TYPE", PS_META_REPLACE, "", type);
+
+    if (paramName) {
+        getId(paramName, argnum, pArgc, argv, options);
+    }
+
+    return itype;
+}
+
+static void doById(int argnum, int *pArgc, char *argv[], psrOptions *options)
+{
+    switch (getType(argnum, pArgc, argv, options, "ID")) {
+    case PSTAMP_RAW:
+        getId("COMPONENT", argnum, pArgc, argv, options);
+        break;
+    case PSTAMP_CHIP:
+        getId("COMPONENT", argnum, pArgc, argv, options);
+        break;
+    case PSTAMP_WARP:
+    case PSTAMP_DIFF:
+    case PSTAMP_STACK:
+        break;
+    default:
+        fprintf(stderr, "programming error unexpected image type\n");
+        exit(1);
+    }
+}
+
+static void doByExp(int argnum, int *pArgc, char *argv[], psrOptions *options)
+{
+    switch (getType(argnum, pArgc, argv, options, "ID")) {
+    case PSTAMP_RAW:
+        getId("COMPONENT", argnum, pArgc, argv, options);
+        break;
+    case PSTAMP_CHIP:
+        getId("COMPONENT", argnum, pArgc, argv, options);
+        break;
+    case PSTAMP_WARP:
+    case PSTAMP_DIFF:
+    case PSTAMP_STACK:
+        break;
+    default:
+        fprintf(stderr, "programming error unexpected image type\n");
+        exit(1);
+    }
+}
+static void doByCoord(int argnum, int *pArgc, char *argv[], psrOptions *options)
+{
+    switch (getType(argnum, pArgc, argv, options, NULL)) {
+    case PSTAMP_RAW:
+        getId("COMPONENT", argnum, pArgc, argv, options);
+        break;
+    case PSTAMP_CHIP:
+        getId("COMPONENT", argnum, pArgc, argv, options);
+        break;
+    case PSTAMP_WARP:
+    case PSTAMP_DIFF:
+    case PSTAMP_STACK:
+        break;
+    default:
+        fprintf(stderr, "programming error unexpected image type\n");
+        exit(1);
+    }
+}
+
+#define PRINT_MULTIPLE_STYLE_ERROR   \
+    fprintf(stderr, "one of -bycoord -byid -byexp -bydiff may be specified\n")
+
+static psrOptions *parseArguments(int argc, char *argv[], int *pExitStatus)
+{
+    psrOptions *options = psAlloc(sizeof(psrOptions));
+    psMetadata *md = initializeTable();
+    int         argnum;
+    bool        gotStyle  = false;
+    bool        needCoord = false;
+    bool        gotCenter = false;
+    bool        gotRange  = false;
+    bool        needROI   = false;
+    // bool        makeStamps= false; XXX unused
+    unsigned    optionMask = PSTAMP_SELECT_IMAGE;
+
+    options->md = md;
+
+    psMetadataAddU32 (md, PS_LIST_TAIL, "ROWNUM", PS_META_REPLACE, "", 1);
+
+    // Job type. 
+    // "stamp" for make postage stamps. 
+    // "get_image" requests that whole images to be retrieved (magic masked for gpc1)
+    // "list_uri" results in a list of matching images
+    if ((argnum = psArgumentGet(argc, argv, "-get_image"))) {
+        psMetadataAddStr (md, PS_LIST_TAIL, "JOB_TYPE", PS_META_REPLACE, "", "get_image");
+        psArgumentRemove(argnum, &argc, argv);
+    } else if ((argnum = psArgumentGet(argc, argv, "-list_uri"))) {
+        psMetadataAddStr (md, PS_LIST_TAIL, "JOB_TYPE", PS_META_REPLACE, "", "list_uri");
+        psArgumentRemove(argnum, &argc, argv);
+    } else {
+        // default JOB_TYPE is stamp
+        psMetadataAddStr (md, PS_LIST_TAIL, "JOB_TYPE", PS_META_REPLACE, "", "stamp");
+        needROI = true;
+        // XXX unused makeStamps = true;
+    }
+
+    if ((argnum = psArgumentGet(argc, argv, "-req_name"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        if (argc < 2) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "value required for request name");
+            usage(PS_EXIT_DATA_ERROR);
+        }
+        options->requestName = argv[argnum];
+        psArgumentRemove(argnum, &argc, argv);
+    } else {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "req_name is required\n");
+        usage(PS_EXIT_DATA_ERROR);
+    }
+
+    if ((argnum = psArgumentGet(argc, argv, "-project"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        if (argc < 2) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "project is required");
+            usage(PS_EXIT_DATA_ERROR);
+        }
+        psMetadataAddStr(md, PS_LIST_TAIL, "PROJECT", PS_META_REPLACE, "", argv[argnum]);
+        psArgumentRemove(argnum, &argc, argv);
+    } else {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "project is required\n");
+        usage(PS_EXIT_DATA_ERROR);
+    }
+
+    // if provided, stamp name tag will be appended to the base name for the postage stamp images
+    if ((argnum = psArgumentGet(argc, argv, "-stamp_name"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        if (argc < 2) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "missing value for stamp_name");
+            usage(PS_EXIT_DATA_ERROR);
+        }
+        psMetadataAddStr(md, PS_LIST_TAIL, "STAMP_NAME", PS_META_REPLACE, "", argv[argnum]);
+        psArgumentRemove(argnum, &argc, argv);
+    }
+
+    if ((argnum = psArgumentGet(argc, argv, "-mask"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        optionMask |= PSTAMP_SELECT_MASK;
+    }
+    if ((argnum = psArgumentGet(argc, argv, "-weight"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        optionMask |= PSTAMP_SELECT_WEIGHT;
+    }
+    psMetadataAddU32(md, PS_LIST_TAIL, "OPTION_MASK", PS_META_REPLACE, "", optionMask);
+
+    // find style & image type
+    if ((argnum = psArgumentGet(argc, argv, "-bycoord"))) {
+        gotStyle = true;
+        psMetadataAddStr(md, PS_LIST_TAIL, "REQ_TYPE", PS_META_REPLACE, "", 1+argv[argnum]);
+
+        psArgumentRemove(argnum, &argc, argv); needCoord = true;
+        needROI = true;
+        doByCoord(argnum, &argc, argv, options);
+    }
+
+    if ((argnum = psArgumentGet(argc, argv, "-byid"))) {
+        if (gotStyle) {
+            PRINT_MULTIPLE_STYLE_ERROR;
+            usage(PS_EXIT_DATA_ERROR);
+        }
+        gotStyle = true;
+        psMetadataAddStr(md, PS_LIST_TAIL, "REQ_TYPE", PS_META_REPLACE, "", 1+argv[argnum]);
+        psArgumentRemove(argnum, &argc, argv);
+        doById(argnum, &argc, argv, options);
+    }
+
+    if ((argnum = psArgumentGet(argc, argv, "-byexp"))) {
+        if (gotStyle) {
+            PRINT_MULTIPLE_STYLE_ERROR;
+            usage(PS_EXIT_DATA_ERROR);
+        }
+        gotStyle = true;
+        psMetadataAddStr(md, PS_LIST_TAIL, "REQ_TYPE", PS_META_REPLACE, "", 1+argv[argnum]);
+        psArgumentRemove(argnum, &argc, argv);
+        doByExp(argnum, &argc, argv, options);
+    } 
+
+    if ((argnum = psArgumentGet(argc, argv, "-bydiff"))) {
+        if (gotStyle) {
+            PRINT_MULTIPLE_STYLE_ERROR;
+            usage(PS_EXIT_DATA_ERROR);
+        }
+        gotStyle = true;
+        psMetadataAddStr(md, PS_LIST_TAIL, "REQ_TYPE", PS_META_REPLACE, "", 1+argv[argnum]);
+        psArgumentRemove(argnum, &argc, argv);
+        // It looks like byExp and byId are identical, so reuse them
+        doByExp(argnum, &argc, argv, options);
+    } 
+
+    if (!gotStyle) {
+        fprintf(stderr, "one of -bycoord -byid -byexp -bydiff must be specified\n");
+        usage(PS_EXIT_DATA_ERROR);
+    }
+
+    if ((argnum = psArgumentGet(argc, argv, "-mjd_min"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        if (argc < 2) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "missing value mjd_min");
+            usage(PS_EXIT_DATA_ERROR);
+        }
+        psMetadataAddF64(md, PS_LIST_TAIL, "MJD_MIN", PS_META_REPLACE, "", atof(argv[argnum]));
+        psArgumentRemove(argnum, &argc, argv);
+    } 
+    if ((argnum = psArgumentGet(argc, argv, "-mjd_max"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        if (argc < 2) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "missing value mjd_max");
+            usage(PS_EXIT_DATA_ERROR);
+        }
+        psMetadataAddF64(md, PS_LIST_TAIL, "MJD_MAX", PS_META_REPLACE, "", atof(argv[argnum]));
+        psArgumentRemove(argnum, &argc, argv);
+    }
+
+    pstampROI roiParam;
+
+    // try and parse the ROI even if we don't "need it" to absorb arguments if they were provided
+    if (!pstampGetROI(&roiParam, &argc, argv, &gotCenter, &gotRange)) {
+        if (needROI) {
+            usage(PS_EXIT_DATA_ERROR);
+        } else {
+            psErrorClear();
+        }
+    }
+
+    if (needROI) {
+        unsigned coord_mask = 0;
+
+        if (roiParam.celestialCenter) {
+            psMetadataAddF64 (md, PS_LIST_TAIL, "CENTER_X", PS_META_REPLACE, "",
+                RAD_TO_DEG(roiParam.centerRA));
+            psMetadataAddF64 (md, PS_LIST_TAIL, "CENTER_Y", PS_META_REPLACE, "",
+                RAD_TO_DEG(roiParam.centerDEC));
+        } else {
+            if (needCoord) {
+                fprintf(stderr, "need to specify ROI in sky coordinates with -bycoord\n");
+                usage(PS_EXIT_DATA_ERROR);
+            }
+            coord_mask |= PSTAMP_CENTER_IN_PIXELS;
+            psMetadataAddF64 (md, PS_LIST_TAIL, "CENTER_X", PS_META_REPLACE, "", atof(roiParam.center[0]));
+            psMetadataAddF64 (md, PS_LIST_TAIL, "CENTER_Y", PS_META_REPLACE, "", atof(roiParam.center[1]));
+        }
+
+        if (!roiParam.celestialRange) {
+            coord_mask |= PSTAMP_RANGE_IN_PIXELS;
+        }
+        psMetadataAddF64 (md, PS_LIST_TAIL, "WIDTH", PS_META_REPLACE, "",  atof(roiParam.range[0]));
+        psMetadataAddF64 (md, PS_LIST_TAIL, "HEIGHT", PS_META_REPLACE, "", atof(roiParam.range[1]));
+
+        psMetadataAddU32(md, PS_LIST_TAIL, "COORD_MASK", PS_META_REPLACE, "", coord_mask);
+    }
+
+    // only argument left should be the required file name for the request file
+
+    if (argc == 2) {
+        options->fileName = psStringCopy(argv[1]);
+    } else if (argc == 1) {
+        fprintf(stderr, "output file name is required\n");
+        usage(PS_EXIT_DATA_ERROR);
+    } else {
+        fprintf(stderr, "too many arguments supplied:");
+        fprintf(stderr, " %s", argv[1]);
+        for (int i=2; i<argc; i++) {
+            fprintf(stderr, ", %s", argv[i]);
+        }
+        fprintf(stderr, "\n");
+        usage(PS_EXIT_DATA_ERROR);
+    }
+
+    return options;
+}
+
+static bool writeTable(psrOptions *options, int *pExitStatus)
+{
+    psFits *fitsFile = psFitsOpen(options->fileName, "w");
+    if (fitsFile == NULL) {
+        psError(PS_ERR_IO, true, "failed to open %s for output\n", options->fileName);
+        *pExitStatus = PS_EXIT_SYS_ERROR;
+        return false;
+    }
+    psMetadata *header = psMetadataAlloc();
+
+    psMetadataAddStr(header, PS_LIST_TAIL, "EXTVER",   PS_META_REPLACE, "", STAMP_REQUEST_VERSION);
+    psMetadataAddStr(header, PS_LIST_TAIL, "REQ_NAME", PS_META_REPLACE, "", options->requestName);
+
+    psArray    *table = psArrayAlloc(1);
+    table->data[0] = options->md;
+
+    if (!psFitsWriteTable(fitsFile, header, table, STAMP_REQUEST_EXTNAME)) {
+        psError(PS_ERR_IO, false, "failed to write fits table");
+        *pExitStatus = PS_EXIT_SYS_ERROR;
+        return false;
+    }
+
+    if (! psFitsClose(fitsFile)) {
+        psError(PS_ERR_IO, false, "failed to close fits table");
+        *pExitStatus = PS_EXIT_SYS_ERROR;
+        return false;
+    }
+
+    return true;
+}
+
+int main(int argc, char *argv[])
+{
+    int exitStatus = 0;
+
+    // all of the action happens in parseArguments
+    psrOptions *options = parseArguments(argc, argv, &exitStatus);
+
+    if (!options) {
+        return 1;
+    }
+
+    if (options->verbose) {
+        psMetadataPrint(stderr, options->md, 0);
+    }
+
+    if (!writeTable(options, &exitStatus)) {
+        psErrorStackPrint(stderr, "failed to create request table");
+    }
+    return exitStatus;
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/README
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/README	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/README	(revision 41103)
@@ -0,0 +1,49 @@
+This directory contains scripts for creating sample MOPS_DETECTABILTY_QUERY
+fits tables into a Data Store
+
+    testdquery.pl --query_id id [--input query_descriptor_file]
+
+Creates a detectability query table with a given QUERY_ID from a textual
+description. See below for format. If no file is provided a sample is 
+used. The request file is added to the data store product with a fileset
+name equal to the query_id
+
+testdquery.pl uses dsreg to register the fileset and thus the IPP
+configuration must be set up.
+
+The request file is created by the program
+
+    detect_query_create id  --input query_descriptor_file \
+                            --output query_file_name \
+                            [--query_id id]
+    
+
+This program creates a request file with given name and query id from the
+request descriptor file
+
+The request descriptor file format is simple. 
+
+Blank lines and lines that begin with the hash character '#' are ignored.
+
+The first valid line must contain 6 whitespace separated fields. These
+data are used to set the header keywords for the Query
+
+# QUERY_ID EXTVER FPA_ID       MJD-OBS FILTER OBSCODE
+QUERY42       1   o4608g0103o   54608     g     566
+
+If the command line option --query_id is provided it overrides the value
+in the descriptor file.
+
+Subsequent lines in the file are used to define the rows of the query.
+For example:
+
+# ROWNUM     RA1_DEG        DEC1_DEG    RA2_DEG     DEC2_DEG     MAG
+1            312.44049389 30.54022727  312.44051968 30.54024139  18.4228
+2            313.03337881 31.01317194  313.03344194 31.01324268  18.3961
+3            312.91159232 30.95195459  312.91153476 30.95190113  18.1110
+4            312.26742527 30.95207284  312.26744769 30.95206300  17.0908
+5            313.20263734 30.62317841  313.20266984 30.62310935  18.2890
+
+
+
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/asteroid.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/asteroid.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/asteroid.txt	(revision 41103)
@@ -0,0 +1,30 @@
+# Sample Postage stamp request description file
+# This can be parsed by the program pstamp_req_create to build a 
+# PS1_PSTAMP_REQUEST binary table
+
+# First line of data is for the extension header
+# The order of keywords follows TABLE 6 of ICD
+#
+# Note that value for REQ_NAME may be overriden by a command line
+# argument to pstamp_req_create.
+# Blank and comment lines are ignored
+
+# REQ_NAME EXTVER
+CHANGEME     1
+
+# subsequent lines define the rows in the table
+#
+
+# These coordinates get stamps from warp and diff images that show a moving object that mops found.
+
+# ID    |     ROI Specification                   |  JOB Specification | Images of interest specification
+# ROWNUM CENTER_X         CENTER_Y         WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT REQ_TYPE IMG_TYPE    ID   TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX | COMMENT
+#
+1        284.688522833822 -3.86916140936841 200  200        2       stamp    3           gpc1     bydiff   warp      362483  NULL    NULL   null  null    0       0      | 
+2        284.688522833822 -3.86916140936841 200  200        2       stamp    3           gpc1     bydiff   diff      362483  NULL    NULL   null  null    0       0      | 
+3        284.688522833822 -3.86916140936841 200  200        2       stamp    1027        gpc1     bydiff   warp      362483  NULL    NULL   null  null    0       0      | 
+4        284.688522833822 -3.86916140936841 200  200        2       stamp    1027        gpc1     bydiff   diff      362483  NULL    NULL   null  null    0       0      | 
+
+
+
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/bycoord.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/bycoord.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/bycoord.txt	(revision 41103)
@@ -0,0 +1,28 @@
+# Sample Postage stamp request description file
+# This can be parsed by the program pstamp_req_create to build a 
+# PS1_PSTAMP_REQUEST binary table
+
+# First line of data is for the extension header
+# The order of keywords follows TABLE 6 of ICD
+#
+# Note that value for REQ_NAME may be overriden by a command line
+# argument to pstamp_req_create.
+# Blank and comment lines are ignored
+
+# REQ_NAME      EXTVER
+bycoord_test     1
+
+# subsequent lines define the rows in the table
+
+# If ROWNUM is set to zero, pstamp_request_file will set insert a value for each row beginning with 1
+# If a later input row reuses a value an error occurs
+
+# Thsee coordinates are in MD06
+#
+# ID    |     ROI Specification                   |  JOB Specification | Images of interest specification
+# ROWNUM CENTER_X    CENTER_Y  WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT REQ_TYPE IMG_TYPE    ID   TESS_ID COMPONENT    DATA_GROUP  REQFILT MJD_MIN MJD_MAX | COMMENT
+#
+0       183.2375     47.30388 500    500      2        stamp       1        gpc1    bycoord chip       null   null   null           null        null   55240     0   |
+0       183.2375     47.30388 500    500      2        stamp       1        gpc1    bycoord warp      null   null   null           null        null   55240     0   |
+0       183.2375     47.30388 500    500      2        stamp       1        gpc1    bycoord stack      null   null   null           null        null   55240     0   |
+0       183.2375     47.30388 500    500      2        stamp       1        gpc1    bycoord diff      null   null   null           null        null   55240     0   |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/byskycell.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/byskycell.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/byskycell.txt	(revision 41103)
@@ -0,0 +1,24 @@
+# Sample Postage stamp request description file
+# This can be parsed by the program pstamp_request_file to build a PS1_PSTAMP_REQUEST binary table
+
+# First line of data is for the extension header
+# The order of keywords follows TABLE 6 of ICD
+#
+# Note that value for REQ_NAME may be overriden by a command line
+# argument to pstamp_request_create.
+# Blank and comment lines are ignored
+
+# REQ_NAME      EXTVER
+byskycell_test     1
+
+# subsequent lines define the rows in the table
+
+# If ROWNUM is set to zero, pstamp_request_file will set insert a value for each row beginning with 1
+# If a later input row reuses a value an error is preported
+
+# ID    |     ROI Specification                   |  JOB Specification | Images of interest specification
+# ROWNUM CENTER_X         CENTER_Y         WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT REQ_TYPE IMG_TYPE    ID   TESS_ID COMPONENT    LABEL  REQFILT MJD_MIN MJD_MAX | COMMENT
+#
+0        214.509667604725 52.5181290488877 200    200      2        stamp       1        gpc1    byskycell warp      null   MD07   skycell.044  null   i.00000   54938  54939   |i filter
+0        214.509667604725 52.5181290488877 200    200      2        stamp       1        gpc1    byskycell warp      null   MD07   skycell.044  null   r.00000   54950  54951   |r filter
+9        214.509667604725 52.5181290488877 200    200      2        stamp       1        gpc1    byskycell warp      null   MD07   skycell.044  MD07.200905.v1   g.00000   54979.451  54979.55   |g filter use label too
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/detect_query_create
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/detect_query_create	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/detect_query_create	(revision 41103)
@@ -0,0 +1,287 @@
+#!/usr/bin/env perl
+
+# create a MOPS_DETECTABILITY_QUERY fits table from a text description file
+
+use warnings;
+use strict;
+
+use Astro::FITS::CFITSIO qw( :constants );
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+
+use constant EXTNAME => 'MOPS_DETECTABILITY_QUERY'; # Extension name for output table
+
+my ( $input,			# Name of input text file
+     $output,			# Name of output table
+     $query_id, 
+     $nostage,
+     );
+
+GetOptions(
+	   'input|i=s'    => \$input,
+	   'output|o=s'   => \$output,
+	   'query_id|q=s'  => \$query_id,
+           'nostage'      => \$nostage,
+) or pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --input --output",
+           -exitval => 3) unless defined $input and defined $output;
+
+# The header kewords
+my $header = [
+        { name =>  'QUERY_ID', 
+                    writetype => TSTRING, 
+                    comment => 'MOPS Query ID for this batch query',
+                    value => undef
+        },
+        { name =>  'EXTVER', 
+                    writetype => TSTRING, 
+                    comment => 'Extension version',
+                    value => undef
+        },
+        { name =>  'FPA_ID', 
+                    writetype => TSTRING, 
+                    comment => 'orginal FPA_ID used at ingest',
+                    value => undef
+        }, 
+        { name =>  'MJD-OBS', 
+                    writetype => TDOUBLE, 
+                    comment => 'starting time of the exposure, MJD',
+                    value => undef
+        },
+        { name =>  'FILTER', 
+                    writetype => TSTRING, 
+                    comment => 'effective filter use for the exposure',
+                    value => undef
+        },
+        { name =>  'OBSCODE', 
+                    writetype => TSTRING, 
+                    comment => 'site identifier (MPC observatory code)',
+                    value => undef
+        },
+#        { name =>  'STAGE',
+#                    writetype => TSTRING,
+#                    comment => 'processing stage to examine',
+#                    value => undef
+#        }
+];
+unless(defined($nostage)) {
+    push @{ $header },        { name =>  'STAGE',
+				writetype => TSTRING,
+				comment => 'processing stage to examine',
+				value => undef
+    };
+}
+
+# Specification of columns to write
+my $columns = [ 
+        # matching rownum from detectability original request
+        { name => 'ROWNUM',   type => '20A', writetype => TSTRING }, 
+        # coordinate at start of exposure, in degrees
+        { name => 'RA1_DEG',  type => 'D',   writetype => TDOUBLE },
+        # coordinate at start of exposure, in degrees
+        { name => 'DEC1_DEG', type => 'D',   writetype => TDOUBLE },
+        # coordinate at end of exposure, in degrees
+        { name => 'RA2_DEG',  type => 'D',   writetype => TDOUBLE },
+        # coordinate at end of exposure, in degrees
+        { name => 'DEC2_DEG', type => 'D',   writetype => TDOUBLE },
+        # apparent magnitude
+        { name => 'MAG',      type => 'D',   writetype => TDOUBLE },
+];
+
+my $in;
+if ($input eq '-') {
+    $in = \*STDIN;
+} else {
+    open $in, "<$input" or die "cannot open $input for reading";
+}
+
+my @colData;
+foreach (@$columns) {
+    push @colData, [];
+}
+
+
+my $numRows = read_data_for_table($in,'\s+', \@colData, $header); 
+if (!$numRows) {
+    print STDERR "no data in $input\n";
+    exit 1;
+}
+
+# overwrite the QUERY_ID value from the input file with the command
+# line argument
+
+if ($query_id) {
+    $header->[0]->{value} = $query_id;
+}
+
+my $status = make_fits_table($output, EXTNAME, $numRows, \@colData, $columns, $header);
+
+exit $status;
+
+# XXXXX: This should be in a module
+# two utility functions that may be used to create a FITS binary
+# table from hashes describing the header keywords and columns
+
+# read_table_description reads the data for a table from a simple text file
+# make_fits_table writes out the table to a named file
+
+use Astro::FITS::CFITSIO qw( :constants );
+Astro::FITS::CFITSIO::PerlyUnpacking(1);
+
+# A function to build a fits binary table from supplied data 
+# 
+sub make_fits_table {
+        my $output = shift;     # name of output file
+        my $extname = shift;    # extension name
+        my $numRows = shift;    # number of rows in the table
+        my $colData = shift;    # ref to array of arrays containing the data for each column
+        my $columns = shift;    # ref to array of column descriptions (each a hash)
+                                # with keys: name, type, and writetype
+        my $header = shift;     # ref to array of header keyword descriptions - each a hash
+                                # with keys: name, name, writetype, comment, and value
+        my $status = 0;
+
+        die "incorrect arguments" if !defined($columns);
+        # note $header can be nil
+
+        # build arrays for cfitsio
+        my @colNames;			# Names of columns
+        my @colTypes;			# Types of columns
+        my @colWriteType;               # type to use to write
+
+        foreach my $colSpec ( @$columns) {
+            push @colNames, $colSpec->{name};
+            push @colTypes, $colSpec->{type};
+            push @colWriteType, $colSpec->{writetype};
+        }
+
+        if (-e $output) {
+            unlink "$output" or die "failed to remove existing $output";
+        }
+
+        my $outFits = Astro::FITS::CFITSIO::create_file( $output, $status ); # Output file handle
+        check_fitsio( $status );
+
+        $outFits->create_img( 16, 0, undef, $status );
+        check_fitsio( $status );
+
+        # Create the table
+
+        $outFits->create_tbl( BINARY_TBL(), $numRows, scalar @colNames,
+                                \@colNames, \@colTypes, undef, $extname, $status );
+        check_fitsio( $status );
+
+        # if header keyword descriptions were provided add them
+        if ($header) {
+            foreach my $headerword ( @$header ) {
+                my $value = $headerword->{value};
+                unless (defined $value) {
+                    print "Can't find header keyword $headerword\n";
+                    next;
+                }
+                # zap quotation marks
+                $value =~ s/\'//g;
+                my $name    = $headerword->{name};
+                my $type    = $headerword->{writetype};
+                my $comment = $headerword->{comment};
+                $outFits->write_key( $type, $name, $value, $comment, $status );
+                check_fitsio( $status );
+            }
+        }
+
+
+        for (my $i = 0; $i < scalar @colNames; $i++) {
+            my $writeType = $colWriteType[$i];
+            $outFits->write_col( $writeType, $i + 1, 1, 1, $numRows, $colData->[$i], $status );
+            check_fitsio( $status );
+        }
+
+        $outFits->close_file( $status );
+
+        return 0;
+
+} # end of sub make_fits_table
+
+
+
+# read the table contents from a file
+#
+# input text file format:
+#   lines that begin with '#' are comment lines and are skipped.
+#   other lines are data. Each data line is split into fields with the
+#   provided separator
+#
+# if $header is not null header the first non-commented line is read to
+# fill the value for each header keyword. The number of fields must match
+# the number of keywords.
+#
+# Following the optional header data, each data line contains data for each
+# row in the table. The number of fields must match the number of column
+# arrays provided.
+
+sub read_data_for_table {
+    my $in      = shift;    # input file handle
+    my $sep     = shift;    # string containing field separator
+    my $colData = shift;    # reference to an array of arrays for the data
+    my $header  = shift;    # rerence to array of header keyword descriptions
+
+    my $line_num = 0;
+
+    # read data for header if any data is expected
+    if ($header) {
+        my $nhead = @$header;
+        while (my $line = <$in>) {
+            $line_num++;
+            chomp $line;
+            next if !$line;             # skip blank lines
+            next if ($line =~ /^#/);    # skip comment lines
+            my @vals = split /$sep/, $line;
+            my $nvals = @vals;
+            die "number of header columns in input $nvals does not equal expected number of header words $nhead"
+                    if (@vals != @$header);
+
+            for (my $i=0; $i < @$header; $i++) {
+                $header->[$i]->{value} = $vals[$i];
+            }
+
+            last; # only one header line
+        }
+    }
+
+    my $row_num = 0;
+    my $ncols = @$colData;
+    while (my $line = <$in>) {
+        chomp $line;
+        $line_num++;
+        next if !$line;             # skip blank lines
+        next if ($line =~ /^#/);    # skip comment lines
+
+        my @vals = split /$sep/, $line;
+        my $nvals = @vals;
+        die "number of columns $nvals in input does not equal expected number of header "
+                . " words $ncols on line $line_num" if ($nvals != $ncols);
+
+        for (my $col = 0; $col < @$colData; $col++) {
+            $colData->[$col]->[$row_num] = $vals[$col];
+        }
+        $row_num++;
+    }
+
+    # we return the number of rows read
+    return $row_num;
+}
+
+# From Astro::FITS::CFITSIO demo
+sub check_fitsio
+{
+    my $status = shift;		# Status of FITSIO calls
+
+    if ($status != 0) {
+	my $msg;		# Message to output
+	Astro::FITS::CFITSIO::fits_get_errstatus( $status , $msg );
+	die "CFITSIO error: $msg\n";
+    }
+}
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/getimage.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/getimage.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/getimage.txt	(revision 41103)
@@ -0,0 +1,27 @@
+# Sample Postage stamp request description file
+# This can be parsed by the program pstamp_request_file to build a PS1_PSTAMP_REQUEST binary table
+
+# First line of data is for the extension header
+# The order of keywords follows TABLE 6 of ICD
+#
+# Note that value for REQ_NAME may be overriden by a command line
+# argument to pstamp_request_create.
+# Blank and comment lines are ignored
+
+# REQ_NAME      EXTVER
+byskycell_test     1
+
+# subsequent lines define the rows in the table
+
+# If ROWNUM is set to zero, pstamp_request_file will set insert a value for each row beginning with 1
+# If a later input row reuses one of the automatically assigned values an error is reported.
+
+
+# ID    |     ROI Specification                   |  JOB Specification | Images of interest specification
+# ROWNUM CENTER_X         CENTER_Y         WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT REQ_TYPE IMG_TYPE    ID   TESS_ID COMPONENT    LABEL     REQFILT MJD_MIN MJD_MAX | COMMENT
+#
+# various looks at the location of SN candidate PS1-0905001
+#1       243.035580     55.128140          250   250      2        get_image       7        gpc1    byid     chip  20272         null   null  MD08.200905.v1   null    0      0    |
+2       243.035580     55.128140          250   250      2        get_image       3        gpc1    byid     warp  6323          null   skycell.078  MD08.200905.v1   null    0      0    |
+#3       243.035580     55.128140          250   250      2        get_image       3        gpc1    byid     diff  16880         null   null  MD08.200905.v1   null    0      0    |
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.100lines.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.100lines.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.100lines.txt	(revision 41103)
@@ -0,0 +1,128 @@
+#  TEST: bycoord chip. 3 different releases
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         null     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | norelease
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.bycoord.norelease.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.bycoord.norelease.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.bycoord.norelease.txt	(revision 41103)
@@ -0,0 +1,7 @@
+#  TEST: bycoord chip
+#
+#  chip bycoord no survey or release date cuts
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+0         241.54949 55.4273   500  500   2      stamp         65         gpc1      null         null     bycoord  chip  null null     null      null       i%       55674    55697  null      0    0          |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.bycoord.preview.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.bycoord.preview.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.bycoord.preview.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: bycoord chip
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PREVIEW null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         null     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | norelease
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.bycoord.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.bycoord.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.bycoord.txt	(revision 41103)
@@ -0,0 +1,8 @@
+#  TEST: bycoord chip. 3 different releases
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         null     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | norelease
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.PV1     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | GR1
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly     bycoord  chip  null null     null      null       i%     0        0      null      0    0    | nightly
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.bydiff.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.bydiff.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.bydiff.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: bydiff chip
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      MD08         null     bydiff    chip  16124276 null null   null       i   0    0      null      0        0    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.byexp.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.byexp.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.byexp.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: byexp chip
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      MD08         null     byexp    chip  o6136g0055o null null   null       null   0    0      null      0        0    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.byid.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.byid.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/chip.byid.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: byid chip
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      MD08         null     byid    chip  723948 null null   null       null   0    0      null      0        0    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/diff.bydiff.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/diff.bydiff.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/diff.bydiff.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: bydiff diff
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 500  500      2          stamp     65         gpc1      MD08         null     bydiff    diff  16124276 null     null     null       null        0    0     null       0        0    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/diff.byid.inverse.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/diff.byid.inverse.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/diff.byid.inverse.txt	(revision 41103)
@@ -0,0 +1,8 @@
+#  TEST: byid diff
+#
+#  options mask selects inverse but first row is not bothway
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 500  500      2          stamp     1089         gpc1      MD08         null     byid    diff  261112 null     null     null       null        0    0     null       0        0    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/diff.byid.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/diff.byid.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/diff.byid.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: byid diff
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 500  500      2          stamp     65         gpc1      MD08         null     byid    diff  261112 null     null     null       null        0    0     null       0        0    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/diff.difftype.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/diff.difftype.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/diff.difftype.txt	(revision 41103)
@@ -0,0 +1,17 @@
+#  TEST: byid diff
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+
+#  The first row should select both the warp_warp and warp_stack diffs
+0          136.8461  5.6772   500    500      2          stamp     65         gpc1      null     null    byexp    diff  o6415g0059o null     null     null       null        0    0     null       0        0    |
+
+# warp_warp_only
+0          136.8461  5.6772   500    500      2          stamp     65         gpc1      null     null    byexp    diff  o6415g0059o null     null     null       null        0    0     warp_warp       0        0    | diff_mode
+
+# this should get the warp stack diff
+0          136.8461  5.6772   500    500      2          stamp     65         gpc1      null     null    byexp    diff  o6415g0059o null     null     null       null        0    0     warp_stack       0        0    |
+
+# this should get no stamp because there is no stack_stack diff
+0          136.8461  5.6772   500    500      2          stamp     65         gpc1      null     null    byexp    diff  o6415g0059o null     null     null       null        0    0     stack_stack       0        0    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/extver.1.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/extver.1.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/extver.1.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  REQ_NAME EXTVER ACTION USERNAME EMAIL
+CHANGEME 1
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX | COMMENT
+0         241.54949 55.4273  1500  1500       2      stamp      2049      gpc1     byid    stack  875274 null   null      null       null     0      0     | sample v1 request tadpole galaxy i band
+
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/fwhm_cuts.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/fwhm_cuts.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/fwhm_cuts.txt	(revision 41103)
@@ -0,0 +1,7 @@
+#  stack bycoord from MD08.refstack
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP              REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+2         241.54949 55.4273 500  500   2      stamp      2119         gpc1      null         null     bycoord    stack  null null   null      MD08.refstack.20130401       r        0      0        null      0        3.9       | 
+3         241.54949 55.4273 500  500   2      stamp      2119         gpc1      null      MD08.nightly bycoord    stack  null null   null      null                         i        0      0        null      0        3.9       | 
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/raw.bycoord.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/raw.bycoord.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/raw.bycoord.txt	(revision 41103)
@@ -0,0 +1,8 @@
+#  TEST: bycoord raw. 3 different releases (should get the same results)
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         null        bycoord  raw  null null     null      null       i%     0        0      null      0    0    | norelease
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.PV1     bycoord  raw  null null     null      null       i%     0        0      null      0    0    | GR1
+0         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      3PI         3PI.nightly bycoord  raw  null null     null      null       i%     0        0      null      0    0    | nightly
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/raw.byexp.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/raw.byexp.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/raw.byexp.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: byexp raw
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      MD08         null     byexp    raw  o6136g0055o null null   null       null   0    0      null      0        0    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/raw.byid.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/raw.byid.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/raw.byid.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: byid raw
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      MD08         null     byid    raw  176316 null null   null       null   0    0      null      0        0    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.bycoord.3pi.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.bycoord.3pi.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.bycoord.3pi.txt	(revision 41103)
@@ -0,0 +1,7 @@
+#  stack bycoord from 3PI released
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp      2119         gpc1      3PI         null     bycoord    stack  null null   null      null       r        0      0        null      0        10    | tadpole galaxy r band
+2         241.54949 55.4273 1500  1500   2      stamp      2119         gpc1      3PI         null     bycoord    stack  null null   null      null       i        0      0        null      0        10    | tadpole galaxy i band
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.bydiff.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.bydiff.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.bydiff.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: bydiff stack
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+0         241.54949 55.4273 1500  1500   2      stamp      2113         gpc1      3PI         null     bydiff    stack  16124276 null   null      null       i        0      0        null      0        10    | 
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.byid.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.byid.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.byid.txt	(revision 41103)
@@ -0,0 +1,7 @@
+#  TEST: byid stack
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp      2113         gpc1      3PI         null     byid    stack  1422246 null   null      null       r        0      0        null      0        10    | tadpole galaxy r band
+2         241.54949 55.4273 1500  1500   2      stamp      2113         gpc1      3PI         null     byid    stack  1422346 null   null      null       i        0      0        null      0        10    | tadpole galaxy i band
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.byskycell.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.byskycell.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.byskycell.txt	(revision 41103)
@@ -0,0 +1,7 @@
+#  TEST: byid stack
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp      2113         gpc1      3PI         null     byskycell    stack  null RINGS.V3 skycell.2386.085   null       r        0      0        null      0        10    | tadpole galaxy r band
+2         241.54949 55.4273 1500  1500   2      stamp      2113         gpc1      3PI         null     byskycell    stack  null RINGS.V3 skycell.2386.085   null       i        0      0        null      0        10    | tadpole galaxy i band
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.calib.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.calib.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.calib.txt	(revision 41103)
@@ -0,0 +1,9 @@
+#  stack bycoord from 3PI released
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+
+1         241.54949 55.4273 1500  1500   2      stamp      2057         gpc1      null         3PI.PV1     bycoord    stack  null null   null      null       i        0      0        null      0        0    | 3PI no release
+2         241.54949 55.4273 1500  1500   2      stamp      2057         gpc1      null         null        byid      stack  1422346 null   null      null    null      0      0        null      0        0    | 3PI no release
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.invalid.mjd.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.invalid.mjd.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.invalid.mjd.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: bycoord stack
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      MD08         null     bycoord    stack  null null null   null       null   6414    56415      null      0        10    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.md08.preview.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.md08.preview.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.md08.preview.txt	(revision 41103)
@@ -0,0 +1,7 @@
+#  stack bycoord from MD08.refstack
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PREVIEW null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP              REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+2         241.54949 55.4273 1500  1500   2      stamp      2119         gpc1      null         null     bycoord    stack  null null   null      MD08.refstack.20130401       r        0      0        null      0        0       | tadpole galaxy r band
+4         241.54949 55.4273 1500  1500   2      stamp      2119         gpc1      null         null     bycoord    stack  null null   null      MD08.refstack.20130401       z        0      0        null      0        0       | tadpole galaxy z band
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.md08.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.md08.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.md08.txt	(revision 41103)
@@ -0,0 +1,10 @@
+#  stack bycoord from MD08.refstack
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP              REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp      2119         gpc1      null         null     bycoord    stack  null null   null      MD08.refstack.20130401       g        0      0        null      0        0       | tadpole galaxy g band
+2         241.54949 55.4273 1500  1500   2      stamp      2119         gpc1      null         null     bycoord    stack  null null   null      MD08.refstack.20130401       r        0      0        null      0        0       | tadpole galaxy r band
+3         241.54949 55.4273 1500  1500   2      stamp      2119         gpc1      null         null     bycoord    stack  null null   null      MD08.refstack.20130401       i        0      0        null      0        0       | tadpole galaxy i band
+4         241.54949 55.4273 1500  1500   2      stamp      2119         gpc1      null         null     bycoord    stack  null null   null      MD08.refstack.20130401       z        0      0        null      0        0       | tadpole galaxy z band
+5         241.54949 55.4273 1500  1500   2      stamp      2119         gpc1      null         null     bycoord    stack  null null   null      MD08.refstack.20130401       y        0      0        null      0        0       | tadpole galaxy y band
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.sources.type.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.sources.type.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stack.sources.type.txt	(revision 41103)
@@ -0,0 +1,21 @@
+#  stack bycoord from 3PI released
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+
+# this should pick up best (3PI.PV1)
+1         241.54949 55.4273 1500  1500   2      stamp      2057         gpc1      3PI         null     bycoord    stack  null null   null      null       i        0      0        null      0        10    | 3PI no release
+
+# this will find same stack but not get the skycal cmf
+2         241.54949 55.4273 1500  1500   2      stamp      2057         gpc1      null        null     bycoord    stack  null null   null    LAP.ThreePi.20120706%       i        0 0        null      0        10    | no release LAP data group
+
+# this should find MD08 refstack
+3         241.54949 55.4273 1500  1500   2      stamp      2057         gpc1      MD08        null     bycoord    stack  null null   null     null       i        0      0        reference      0        10    | MD08 reference
+
+# test that we avoid the nightly stacks
+4         241.54949 55.4273 1500  1500   2      stamp      2057         gpc1      MD08        null     bycoord    stack  null null   null     null       i        0 0        notnightly      0        10    | no release notnightly
+
+
+# this looks for a deep stack but there are none yet
+5         241.54949 55.4273 1500  1500   2      stamp      2057         gpc1      MD08        null     bycoord    stack  null null   null     null       i        0      0        deep      0        10    | MD08 deep
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.bycoord.all.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.bycoord.all.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.bycoord.all.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: coord stack_summary
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1     359.767   -30.2009     100    100   2      stamp      197063         gpc1      3PI         null     bycoord   stack_summary  null RINGS.V3 null   LAP.ThreePI.20130717%      r        0      0        null      0        0    | 
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.bycoord.m31.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.bycoord.m31.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.bycoord.m31.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: coord stack_summary
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE      ID TESS_ID COMPONENT     DATA_GROUP    REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1     10.68639  42.27728     100    100   2      stamp      65         gpc1      3PI         null     bycoord   stack_summary  null RINGS.V3 null   null     null        0      0        null      0        0    | M31
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.bycoord.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.bycoord.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.bycoord.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: coord stack_summary
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1     359.767   -30.2009     100    100   2  stamp      196673        gpc1      3PI         null     bycoord   stack_summary  null RINGS.V3 null   LAP.ThreePI.20130717%      r        0      0        null      0        0    | 
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.byid.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.byid.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.byid.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: byid stack
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         0         0         0      0   2      stamp      7         gpc1      3PI         null     byid    stack_summary  26992 null   null      null       r        0      0        null      0        0    | 
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.byskycell.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.byskycell.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.byskycell.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: byid stack
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         0         0         0      0   2      stamp            7         gpc1      3PI         null    byskycell  stack_summary  null RINGS.V3 skycell.0635   null    r        0      0      null      0        0    | 
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.getimage.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.getimage.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/stacksummary.getimage.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: coord stack_summary
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE      ID TESS_ID COMPONENT     DATA_GROUP    REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1     10.68639  42.27728     100    100   2      get_image      65         gpc1      3PI         null     bycoord   stack_summary  27283 RINGS.V3 null   LAP.ThreePI.20130717%     null        0      0        null      0        0    | M31
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/test_qub_ps_request_20130426_225546.tbl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/test_qub_ps_request_20130426_225546.tbl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/test_qub_ps_request_20130426_225546.tbl	(revision 41103)
@@ -0,0 +1,17 @@
+SIMPLE  =                    T / file does conform to FITS standard             BITPIX  =                    8 / number of bits per data pixel                  NAXIS   =                    0 / number of data axes                            EXTEND  =                    T / FITS dataset may contain extensions            COMMENT   FITS (Flexible Image Transport System) format is defined in 'AstronomyCOMMENT   and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H END                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             XTENSION= 'BINTABLE'           / binary table extension                         BITPIX  =                    8 / array data type                                NAXIS   =                    2 / number of array dimensions                     NAXIS1  =                  412 / length of dimension 1                          NAXIS2  =                   63 / length of dimension 2                          PCOUNT  =                    0 / number of group parameters                     GCOUNT  =                    1 / number of groups                               TFIELDS =                   19 / number of table fields                         TTYPE1  = 'ROWNUM  '                                                            TFORM1  = 'J       '                                                            TTYPE2  = 'CENTER_X'                                                            TFORM2  = 'D       '                                                            TTYPE3  = 'CENTER_Y'                                                            TFORM3  = 'D       '                                                            TTYPE4  = 'WIDTH   '                                                            TFORM4  = 'D       '                                                            TTYPE5  = 'HEIGHT  '                                                            TFORM5  = 'D       '                                                            TTYPE6  = 'COORD_MASK'                                                          TFORM6  = 'J       '                                                            TTYPE7  = 'JOB_TYPE'                                                            TFORM7  = '16A     '                                                            TTYPE8  = 'OPTION_MASK'                                                         TFORM8  = 'J       '                                                            TTYPE9  = 'PROJECT '                                                            TFORM9  = '16A     '                                                            TTYPE10 = 'REQ_TYPE'                                                            TFORM10 = '16A     '                                                            TTYPE11 = 'IMG_TYPE'                                                            TFORM11 = '16A     '                                                            TTYPE12 = 'ID      '                                                            TFORM12 = '16A     '                                                            TTYPE13 = 'TESS_ID '                                                            TFORM13 = '64A     '                                                            TTYPE14 = 'COMPONENT'                                                           TFORM14 = '64A     '                                                            TTYPE15 = 'LABEL   '                                                            TFORM15 = '64A     '                                                            TTYPE16 = 'REQFILT '                                                            TFORM16 = '16A     '                                                            TTYPE17 = 'MJD_MIN '                                                            TFORM17 = 'D       '                                                            TTYPE18 = 'MJD_MAX '                                                            TFORM18 = 'D       '                                                            TTYPE19 = 'COMMENT '                                                            TFORM19 = '64A     '                                                            EXTNAME = 'PS1_PS_REQUEST'     / name of this binary table extension            REQ_NAME= 'test_qub_ps_request_20130426_225546' / Postage Stamp request name    EXTVER  = '1       '           / Extension version                              HISTORY File modified by user 'bills' with fv  on 2013-04-26T13:24:49           END                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                @jº7³÷@JE	èã@rÀ     @rÀ        stamp             gpc1            byid            diff            423090          MD07                                                            skycell.034                                                     null                                                            null                            1141516631523220800_56402.560_25666082_2_diff                      @jº7³÷@JE	èã@rÀ     @rÀ        stamp             gpc1            byid            stack           1964771         MD07                                                            skycell.034                                                     null                                                            null                            1141516631523220800_56402.560_25666082_2_ref                       @jº7³÷@JE	èã@rÀ     @rÀ        stamp             gpc1            byid            stack           2211920         MD07                                                            skycell.034                                                     null                                                            null                            1141516631523220800_56402.560_25666082_2_target                    @jº7œ©@JDÿ7ü§@rÀ     @rÀ        stamp             gpc1            byid            diff            427003          MD07                                                            skycell.034                                                     null                                                            null                            1141516631523220800_56408.416_25881073_81_diff                     @jº7œ©@JDÿ7ü§@rÀ     @rÀ        stamp             gpc1            byid            stack           1964705         MD07                                                            skycell.034                                                     null                                                            null                            1141516631523220800_56408.416_25881073_81_ref                      @jº7œ©@JDÿ7ü§@rÀ     @rÀ        stamp             gpc1            byid            stack           2220425         MD07                                                            skycell.034                                                     null                                                            null                            1141516631523220800_56408.416_25881073_81_target                   @jº­³T\	@JDÕ  +@rÀ     @rÀ        stamp             gpc1            byid            diff            423090          MD07                                                            skycell.034                                                     null                                                            null                            1141520081523215900_56402.560_25666082_380_diff                    @jº­³T\	@JDÕ  +@rÀ     @rÀ        stamp             gpc1            byid            stack           1964771         MD07                                                            skycell.034                                                     null                                                            null                            1141520081523215900_56402.560_25666082_380_ref                     	@jº­³T\	@JDÕ  +@rÀ     @rÀ        stamp             gpc1            byid            stack           2211920         MD07                                                            skycell.034                                                     null                                                            null                            1141520081523215900_56402.560_25666082_380_target                  
+@jº¯+Ä@JDÖª*@rÀ     @rÀ        stamp             gpc1            byid            diff            427003          MD07                                                            skycell.034                                                     null                                                            null                            1141520081523215900_56408.416_25881073_19_diff                     
+@jº¯+Ä@JDÖª*@rÀ     @rÀ        stamp             gpc1            byid            stack           1964705         MD07                                                            skycell.034                                                     null                                                            null                            1141520081523215900_56408.416_25881073_19_ref                      
+@jº¯+Ä@JDÖª*@rÀ     @rÀ        stamp             gpc1            byid            stack           2220425         MD07                                                            skycell.034                                                     null                                                            null                            1141520081523215900_56408.416_25881073_19_target                   
+@jÀR<`Û@Jdåé
+sÁ@rÀ     @rÀ        stamp             gpc1            byid            diff            423090          MD07                                                            skycell.034                                                     null                                                            null                            1141600911524717700_56402.560_25666082_300_diff                    @jÀR<`Û@Jdåé
+sÁ@rÀ     @rÀ        stamp             gpc1            byid            stack           1964771         MD07                                                            skycell.034                                                     null                                                            null                            1141600911524717700_56402.560_25666082_300_ref                     @jÀR<`Û@Jdåé
+sÁ@rÀ     @rÀ        stamp             gpc1            byid            stack           2211920         MD07                                                            skycell.034                                                     null                                                            null                            1141600911524717700_56402.560_25666082_300_target                  @jÀ *N6@Jdâox@rÀ     @rÀ        stamp             gpc1            byid            diff            427003          MD07                                                            skycell.034                                                     null                                                            null                            1141600911524717700_56408.416_25881073_101_diff                    @jÀ *N6@Jdâox@rÀ     @rÀ        stamp             gpc1            byid            stack           1964705         MD07                                                            skycell.034                                                     null                                                            null                            1141600911524717700_56408.416_25881073_101_ref                     @jÀ *N6@Jdâox@rÀ     @rÀ        stamp             gpc1            byid            stack           2220425         MD07                                                            skycell.034                                                     null                                                            null                            1141600911524717700_56408.416_25881073_101_target                  @j§ÐÅ9{@J?å@òÍ­@rÀ     @rÀ        stamp             gpc1            byid            diff            416773          MD07                                                            skycell.035                                                     null                                                            null                            1141258611522957000_56392.509_25353215_4_diff                      @j§ÐÅ9{@J?å@òÍ­@rÀ     @rÀ        stamp             gpc1            byid            stack           1964772         MD07                                                            skycell.035                                                     null                                                            null                            1141258611522957000_56392.509_25353215_4_ref                       @j§ÐÅ9{@J?å@òÍ­@rÀ     @rÀ        stamp             gpc1            byid            stack           2201969         MD07                                                            skycell.035                                                     null                                                            null                            1141258611522957000_56392.509_25353215_4_target                    @j§ÐÈP@J?ãZâ/@rÀ     @rÀ        stamp             gpc1            byid            diff            427003          MD07                                                            skycell.035                                                     null                                                            null                            1141258611522957000_56408.416_25881074_4_diff                      @j§ÐÈP@J?ãZâ/@rÀ     @rÀ        stamp             gpc1            byid            stack           1964706         MD07                                                            skycell.035                                                     null                                                            null                            1141258611522957000_56408.416_25881074_4_ref                       @j§ÐÈP@J?ãZâ/@rÀ     @rÀ        stamp             gpc1            byid            stack           2220426         MD07                                                            skycell.035                                                     null                                                            null                            1141258611522957000_56408.416_25881074_4_target                    @j§Ïù]Ç@J?çx_ó@rÀ     @rÀ        stamp             gpc1            byid            diff            423090          MD07                                                            skycell.035                                                     null                                                            null                            1141258611522957000_56402.560_25666083_35_diff                     @j§Ïù]Ç@J?çx_ó@rÀ     @rÀ        stamp             gpc1            byid            stack           1964772         MD07                                                            skycell.035                                                     null                                                            null                            1141258611522957000_56402.560_25666083_35_ref                      @j§Ïù]Ç@J?çx_ó@rÀ     @rÀ        stamp             gpc1            byid            stack           2211921         MD07                                                            skycell.035                                                     null                                                            null                            1141258611522957000_56402.560_25666083_35_target                   
+@j§Ñ|[w(@J?ãBV@rÀ     @rÀ        stamp             gpc1            byid            diff            419665          MD07                                                            skycell.035                                                     null                                                            null                            1141258611522957000_56397.431_25498242_17_diff                     
+@j§Ñ|[w(@J?ãBV@rÀ     @rÀ        stamp             gpc1            byid            stack           1964772         MD07                                                            skycell.035                                                     null                                                            null                            1141258611522957000_56397.431_25498242_17_ref                      
+@j§Ñ|[w(@J?ãBV@rÀ     @rÀ        stamp             gpc1            byid            stack           2204406         MD07                                                            skycell.035                                                     null                                                            null                            1141258611522957000_56397.431_25498242_17_target                   @jaô¶Æ!@J[ì7Íö[@rÀ     @rÀ        stamp             gpc1            byid            diff            419665          MD07                                                            skycell.035                                                     null                                                            null                            1141125361524305300_56397.431_25498242_49_diff                      @jaô¶Æ!@J[ì7Íö[@rÀ     @rÀ        stamp             gpc1            byid            stack           1964772         MD07                                                            skycell.035                                                     null                                                            null                            1141125361524305300_56397.431_25498242_49_ref                      !@jaô¶Æ!@J[ì7Íö[@rÀ     @rÀ        stamp             gpc1            byid            stack           2204406         MD07                                                            skycell.035                                                     null                                                            null                            1141125361524305300_56397.431_25498242_49_target                   "@jaÑ7Ù8@J[ëxy|@rÀ     @rÀ        stamp             gpc1            byid            diff            427003          MD07                                                            skycell.035                                                     null                                                            null                            1141125361524305300_56408.416_25881074_25_diff                     #@jaÑ7Ù8@J[ëxy|@rÀ     @rÀ        stamp             gpc1            byid            stack           1964706         MD07                                                            skycell.035                                                     null                                                            null                            1141125361524305300_56408.416_25881074_25_ref                      $@jaÑ7Ù8@J[ëxy|@rÀ     @rÀ        stamp             gpc1            byid            stack           2220426         MD07                                                            skycell.035                                                     null                                                            null                            1141125361524305300_56408.416_25881074_25_target                   %@j`ÿ$SŸ@J[éÑV{@rÀ     @rÀ        stamp             gpc1            byid            diff            424459          MD07                                                            skycell.035                                                     null                                                            null                            1141125361524305300_56404.546_25741368_14_diff                     &@j`ÿ$SŸ@J[éÑV{@rÀ     @rÀ        stamp             gpc1            byid            stack           1964772         MD07                                                            skycell.035                                                     null                                                            null                            1141125361524305300_56404.546_25741368_14_ref                      '@j`ÿ$SŸ@J[éÑV{@rÀ     @rÀ        stamp             gpc1            byid            stack           2212743         MD07                                                            skycell.035                                                     null                                                            null                            1141125361524305300_56404.546_25741368_14_target                   (@j`ÞJc@J[éD+£=@rÀ     @rÀ        stamp             gpc1            byid            diff            423090          MD07                                                            skycell.035                                                     null                                                            null                            1141125361524305300_56402.560_25666083_34_diff                     )@j`ÞJc@J[éD+£=@rÀ     @rÀ        stamp             gpc1            byid            stack           1964772         MD07                                                            skycell.035                                                     null                                                            null                            1141125361524305300_56402.560_25666083_34_ref                      *@j`ÞJc@J[éD+£=@rÀ     @rÀ        stamp             gpc1            byid            stack           2211921         MD07                                                            skycell.035                                                     null                                                            null                            1141125361524305300_56402.560_25666083_34_target                   +@jq^PTxá@JX±óvI
+@rÀ     @rÀ        stamp             gpc1            byid            diff            410657          MD07                                                            skycell.037                                                     null                                                            null                            1140610261524134500_56383.565_25053115_30_diff                     ,@jq^PTxá@JX±óvI
+@rÀ     @rÀ        stamp             gpc1            byid            stack           1964774         MD07                                                            skycell.037                                                     null                                                            null                            1140610261524134500_56383.565_25053115_30_ref                      -@jq^PTxá@JX±óvI
+@rÀ     @rÀ        stamp             gpc1            byid            stack           2196005         MD07                                                            skycell.037                                                     null                                                            null                            1140610261524134500_56383.565_25053115_30_target                   .@jq_d`
+k@JX³(:;@rÀ     @rÀ        stamp             gpc1            byid            diff            427003          MD07                                                            skycell.037                                                     null                                                            null                            1140610261524134500_56408.416_25881076_37_diff                     /@jq_d`
+k@JX³(:;@rÀ     @rÀ        stamp             gpc1            byid            stack           1964708         MD07                                                            skycell.037                                                     null                                                            null                            1140610261524134500_56408.416_25881076_37_ref                      0@jq_d`
+k@JX³(:;@rÀ     @rÀ        stamp             gpc1            byid            stack           2220428         MD07                                                            skycell.037                                                     null                                                            null                            1140610261524134500_56408.416_25881076_37_target                   1@jq]i(Š@JX®k<Ú@rÀ     @rÀ        stamp             gpc1            byid            diff            424459          MD07                                                            skycell.037                                                     null                                                            null                            1140610261524134500_56404.546_25741370_9_diff                      2@jq]i(Š@JX®k<Ú@rÀ     @rÀ        stamp             gpc1            byid            stack           1964774         MD07                                                            skycell.037                                                     null                                                            null                            1140610261524134500_56404.546_25741370_9_ref                       3@jq]i(Š@JX®k<Ú@rÀ     @rÀ        stamp             gpc1            byid            stack           2212745         MD07                                                            skycell.037                                                     null                                                            null                            1140610261524134500_56404.546_25741370_9_target                    4@jq^·]@JX°k¶@rÀ     @rÀ        stamp             gpc1            byid            diff            423090          MD07                                                            skycell.037                                                     null                                                            null                            1140610261524134500_56402.560_25666085_42_diff                     5@jq^·]@JX°k¶@rÀ     @rÀ        stamp             gpc1            byid            stack           1964774         MD07                                                            skycell.037                                                     null                                                            null                            1140610261524134500_56402.560_25666085_42_ref                      6@jq^·]@JX°k¶@rÀ     @rÀ        stamp             gpc1            byid            stack           2211923         MD07                                                            skycell.037                                                     null                                                            null                            1140610261524134500_56402.560_25666085_42_target                   7@jq^÷ÉT(@JXŽí*H@rÀ     @rÀ        stamp             gpc1            byid            diff            419665          MD07                                                            skycell.037                                                     null                                                            null                            1140610261524134500_56397.431_25498244_45_diff                     8@jq^÷ÉT(@JXŽí*H@rÀ     @rÀ        stamp             gpc1            byid            stack           1964774         MD07                                                            skycell.037                                                     null                                                            null                            1140610261524134500_56397.431_25498244_45_ref                      9@jq^÷ÉT(@JXŽí*H@rÀ     @rÀ        stamp             gpc1            byid            stack           2204408         MD07                                                            skycell.037                                                     null                                                            null                            1140610261524134500_56397.431_25498244_45_target                   :@já<;@Ju£œIß@rÀ     @rÀ        stamp             gpc1            byid            diff            422486          MD07                                                            skycell.042                                                     null                                                            null                            1142009271525508600_56401.503_25632812_13_diff                     ;@já<;@Ju£œIß@rÀ     @rÀ        stamp             gpc1            byid            stack           1964910         MD07                                                            skycell.042                                                     null                                                            null                            1142009271525508600_56401.503_25632812_13_ref                      <@já<;@Ju£œIß@rÀ     @rÀ        stamp             gpc1            byid            stack           2210439         MD07                                                            skycell.042                                                     null                                                            null                            1142009271525508600_56401.503_25632812_13_target                   =@já;µÐ@Ju£ÐÞ@rÀ     @rÀ        stamp             gpc1            byid            diff            427003          MD07                                                            skycell.042                                                     null                                                            null                            1142009271525508600_56408.416_25881079_7_diff                      >@já;µÐ@Ju£ÐÞ@rÀ     @rÀ        stamp             gpc1            byid            stack           1964712         MD07                                                            skycell.042                                                     null                                                            null                            1142009271525508600_56408.416_25881079_7_ref                       ?@já;µÐ@Ju£ÐÞ@rÀ     @rÀ        stamp             gpc1            byid            stack           2220432         MD07                                                            skycell.042                                                     null                                                            null                            1142009271525508600_56408.416_25881079_7_target                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/tests_todo
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/tests_todo	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/tests_todo	(revision 41103)
@@ -0,0 +1,10 @@
+each img_type
+each req_type
+        make a request using each of the modes
+        add qualifiers
+    test options
+    test using data from various epochs
+    test coordinates that are outside of the selected image to insure error code is ok
+both request versions
+a few samples were done
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.bycoord.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.bycoord.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.bycoord.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: bycoord warp
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      MD08         null     bycoord    warp  null null null   null       i   56136    56137      null      0        10    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.bycoord.wrongskycell.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.bycoord.wrongskycell.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.bycoord.wrongskycell.txt	(revision 41103)
@@ -0,0 +1,7 @@
+#  TEST: bycoord warp coordinates do not match skycell_id 
+#  should get no overlap error
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      MD08         null     bycoord    warp  null null skycell.065   null       i   56136    56137      null      0        10    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.bydiff.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.bydiff.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.bydiff.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: bydiff stack
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      MD08         null     bydiff    warp  16124276 null null   null       i   0    0      null      0        10    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.byexp.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.byexp.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.byexp.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: byid stack
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      MD08         null     byid    warp  471197 null null   null       i   0    0      null      0        10    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.byid.calib.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.byid.calib.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.byid.calib.txt	(revision 41103)
@@ -0,0 +1,7 @@
+#  TEST: byexp warp
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y   WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         18.876667 -0.861389 1500  1500   2   stamp    1                gpc1        3PI         null     byexp    warp  o6224g0366o null null   null       null   0        0      null      0        0     |
+2        232.9818    62.7904   500  500    2   stamp    1                gpc1        3PI         null     byexp    warp  o6511g0013o null null   null       null   0        0      null      0        0     |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.byid.noverlap.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.byid.noverlap.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.byid.noverlap.txt	(revision 41103)
@@ -0,0 +1,9 @@
+#  TEST: byid warp no overlap. Coordinates are in skycell.065 not the
+#  requested skycell.066.
+#  Note that this causes a job to be queued even though the result is going to
+#  be PSTAMP_NO_OVERLAP
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         242.54949 55.4273 1500  1500   2      stamp         65         gpc1      MD08         null     byid    warp  471197 null skycell.066   null       i   0    0      null      0        0    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.byid.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.byid.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.byid.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: byexp warp
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      MD08         null     byexp    warp  o6136g0055o null null   null       i   0    0      null      0        10    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.byskycell.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.byskycell.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.byskycell.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: byskycell warp
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      MD08         null     byskycell    warp  null MD08.V3 skycell.066   null       i   56136    56137      null      0        10    | tadpole galaxy i band
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.invalidmjd.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.invalidmjd.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.invalidmjd.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: bycoord warp
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      MD08         null     bycoord    warp  null null null   null       null   6414    56415      null      0        10    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.md08.56414.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.md08.56414.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1/warp.md08.56414.txt	(revision 41103)
@@ -0,0 +1,6 @@
+#  TEST: bycoord warp
+#
+#  REQ_NAME EXTVER ACTION EMAIL
+CHANGEME 2 PROCESS null
+#  ROWNUM CENTER_X CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT SURVEY_NAME IPP_RELEASE REQ_TYPE IMG_TYPE ID TESS_ID COMPONENT DATA_GROUP REQFILT MJD_MIN MJD_MAX RUN_TYPE FWHM_MIN FWHM_MAX | COMMENT
+1         241.54949 55.4273 1500  1500   2      stamp         65         gpc1      MD08         null     bycoord    warp  null null null   null       null   56414    56415      null      0        10    |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1_sample.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1_sample.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/gpc1_sample.txt	(revision 41103)
@@ -0,0 +1,45 @@
+# Sample Postage stamp request description file
+# This can be parsed by the program pstamp_req_create to build a 
+# PS1_PSTAMP_REQUEST binary table
+
+# First line of data is for the extension header
+# The order of keywords follows TABLE 6 of ICD
+#
+# Note that value for REQ_NAME may be overriden by a command line
+# argument to pstamp_req_create.
+# Blank and comment lines are ignored
+
+# REQ_NAME EXTVER
+CHANGEME     1
+
+# subsequent lines define the rows in the table
+
+## ID    |     ROI Specification                   |  JOB Specification | Images of interest specification 
+# ROWNUM CENTER_X       CENTER_Y WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT REQ_TYPE IMG_TYPE    ID   TESS_ID COMPONENT  DATA_GROUP REQFILT MJD_MIN MJD_MAX | COMMENT
+# warps from various epochs for one of the SN candidates. Specifiying the skycell speeds up processing
+1        242.400666     55.273513 200   200     2          stamp    1          gpc1   byid      warp     6245   null    skycell.077   null     null     0        0     |
+2        242.400666     55.273513 200   200     2          stamp    1          gpc1   byid      warp     6254   null    skycell.077   null     null     0        0     |
+3        242.400666     55.273513 200   200     2          stamp    1          gpc1   byid      warp     6264   null    skycell.077   null     null     0        0     |
+4        242.400666     55.273513 200   200     2          stamp    1          gpc1   byid      warp     6317   null    skycell.077   null     null     0        0     |
+5        242.400666     55.273513 200   200     2          stamp    1          gpc1   byid      warp     6324   null    skycell.077   null     null     0        0     |
+6        242.400666     55.273513 200   200     2          stamp    1          gpc1   byid      warp     6465   null    skycell.077   null     null     0        0     |
+7        242.400666     55.273513 200   200     2          stamp    1          gpc1   byid      warp     6466   null    skycell.077   null     null     0        0     |
+
+# get a specific warp
+10        242.400666     55.273513 200   200     2          stamp    1         gpc1   byid      warp     6316   null     skycell.077   null     null     0        0     |
+
+# get stamps from all warps for exposure (only destreaked ones will succeed)
+11        242.400666     55.273513 200   200     2          stamp    1         gpc1   byexp     warp  o4973g0123o   null skycell.077   null     null     0        0     |
+
+# get stamps from all chipRuns for exposure (only destreaked ones will succeed)
+# add the mask and weight images as well
+12        242.400666     55.273513 200   200     2          stamp    7         gpc1   byexp  chip  o4973g0123o   null    skycell.077   null     null     0        0     |
+
+# get the corresponding diff
+13        242.400666     55.273513 200   200     2          stamp    1        gpc1   byexp   diff  o4973g0123o   null    null         null     null     0        0     |
+
+# get the stack that was the template for that stack
+14        242.400666     55.273513 200   200     2          stamp    1         gpc1   byid    stack      14031   null     null        null      null     0        0     |
+
+# get the same stamp by go through the diff
+15        242.400666     55.273513 200   200     2          stamp    1         gpc1   bydiff   stack    193939   null     null        null      null     0        0     |
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/maketestreq
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/maketestreq	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/maketestreq	(revision 41103)
@@ -0,0 +1,160 @@
+#!/bin/env perl
+###
+### maketest
+###
+###     Program to take a text format postage stamp request specification
+###     and generate a postage stamp request fits file and optionally submit it for processing
+###
+
+use warnings;
+use strict;
+
+use Getopt::Long qw( GetOptions ) ; # :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+use PS::IPP::Config qw( :standard );
+use PS::IPP::PStamp::RequestFile qw( :standard );
+use PS::IPP::PStamp::Job qw( :standard );
+use File::Temp qw(tempfile);
+use File::Basename qw(basename);
+use IPC::Cmd 0.36 qw( can_run run );
+use Carp;
+use POSIX;
+
+my $missing_tools;
+my $pstamp_request_file  = can_run('pstamp_request_file')  or (warn "Can't find required program pstamp_request_file"  and $missing_tools = 1);
+my $pstamptool  = can_run('pstamptool')  or (warn "Can't find required program pstamptool"  and $missing_tools = 1);
+my $pstampdump  = can_run('pstampdump')  or (warn "Can't find required program pstampdump"  and $missing_tools = 1);
+if ($missing_tools) {
+    warn("Can't find required tools.");
+    exit ($PS_EXIT_CONFIG_ERROR);
+}
+
+
+my ($input, $submit, $listfile, $delete, $simple, $tag_req_name, $req_name, $email);
+my ($dbname, $dbserver);
+my ($save_temps, $verbose);
+
+my $req_name_base = 'test.';
+my $label = 'TEST';
+my $outdir = $ENV{PWD};
+
+GetOptions(
+    'input|i=s'         => \$input,
+    'outdir=s'          => \$outdir,
+    'submit|s'          => \$submit,
+    'req_name=s'        => \$req_name,
+    'req_name_base=s'   => \$req_name_base,
+    'tag_req_name'      => \$tag_req_name,
+    'label=s'           => \$label,
+    'email=s'           => \$email,
+    'username=s'        => \$email, # I always forget what the option is
+
+    'dbname=s'           => \$dbname,
+    'dbserver=s'         => \$dbserver,
+
+    'listfile|l'        => \$listfile,
+    'simple'            => \$simple,
+    'delete'            => \$delete,        # delete request file (after listing perhaps disabled if $submit
+
+    'verbose|v'         => \$verbose,
+    'save-temps'        => \$save_temps,
+) or pod2usage(2);
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+
+pod2usage( -msg => "Required options: --input") 
+    unless (defined $input) ;
+pod2usage( -msg => "Required options: --req_name or --req_name_base") 
+    unless (defined $req_name or defined $req_name_base);
+
+die ("Input file $input not found\n") unless (-e $input);
+die ("Input file $input not readable\n") unless (-r $input);
+
+if (!$dbname or !$dbserver) {
+    my $ipprc = PS::IPP::Config->new('GPC1');
+
+    if (!$dbserver) {
+        $dbserver =  metadataLookupStr($ipprc->{_siteConfig}, 'PS_DBSERVER');
+        die ("couldn't find dbserver in site.config\n") unless $dbserver;
+    }
+    if (!$dbname) {
+        $dbname =  metadataLookupStr($ipprc->{_siteConfig}, 'PS_DBNAME');
+        die ("couldn't find dbname in site.config\n") unless $dbname;
+    }
+}
+$pstamptool .= " -dbserver $dbserver -dbname $dbname";
+
+
+# if request name was not supplied make one
+if (!$req_name) {
+    if ($tag_req_name) {
+        # use the input file name with .txt removed as the tag
+        my $tag = $input;
+        $tag =~ s/\.txt$//;
+        $req_name_base = "$req_name_base$tag.";
+    }
+    my $datestr = strftime "%Y%m%dT%H%M%S", gmtime;
+    $req_name .= $req_name_base . $datestr;
+}
+
+my $table_extension = 'tbl';
+
+if (! ($outdir =~ /^\//) ) {
+    # turn relative path into absolute
+    $outdir = $ENV{PWD} . "/$outdir";
+}
+
+my $request_file_name = "$outdir/$req_name.$table_extension";
+
+# build the request fits table
+{
+    my $command = "$pstamp_request_file --input $input --output $request_file_name";
+    $command .= " --req_name $req_name" if $req_name;
+    $command .= " --email $email" if $email;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        print STDERR @$stderr_buf;
+        exit $error_code >> 8;
+    }
+}
+die ("cannot find request file $request_file_name\n") unless -e $request_file_name;
+
+# optionally use pstampdump to show the contents of the request file
+if ($listfile) {
+    my $command = "$pstampdump -header $request_file_name";
+    $command .= " -simple" if $simple;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        print STDERR @$stderr_buf;
+        exit $error_code >> 8;
+    }
+    print join "", @$stdout_buf;
+}
+
+# optionally submit the request by adding it to the database.
+# Note that once the request has been processed the request file is copied to the postage stamp working
+# directories so these files may be deleted.
+if ($submit) {
+    my $command = "$pstamptool -addreq -uri $request_file_name -label $label";
+    $command .= " -username $email" if $email;
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        print STDERR @$stderr_buf;
+        exit $error_code >> 8;
+    }
+    my $output = join "", @$stdout_buf;
+    print "RequestFile $request_file_name submitted. req_id $output";
+} else {
+    # we aren't submitting the file that we just built
+    # optionally delete it
+    if ($delete) {
+        unlink $request_file_name or die "failed to delete request file $request_file_name\n";
+    } else {
+        print "RequestFile $request_file_name\n";
+    }
+}
+
+exit 0;
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/ps1-0811001.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/ps1-0811001.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/ps1-0811001.txt	(revision 41103)
@@ -0,0 +1,26 @@
+# Sample Postage stamp request description file
+# This can be parsed by the program pstamp_request_file to build a PS1_PSTAMP_REQUEST binary table
+
+# First line of data is for the extension header
+# The order of keywords follows TABLE 6 of ICD
+#
+# Note that value for REQ_NAME may be overriden by a command line
+# argument to pstamp_request_create.
+# Blank and comment lines are ignored
+
+# REQ_NAME      EXTVER
+byskycell_test     1
+
+# subsequent lines define the rows in the table
+
+# If ROWNUM is set to zero, pstamp_request_file will set insert a value for each row beginning with 1
+# If a later input row reuses a value an error is preported
+
+# ID    |     ROI Specification                   |  JOB Specification | Images of interest specification
+# ROWNUM CENTER_X         CENTER_Y         WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT REQ_TYPE IMG_TYPE    ID   TESS_ID COMPONENT    LABEL  REQFILT MJD_MIN MJD_MAX | COMMENT
+#
+0         36.945000     -4.579750          250   250      2        stamp       1        gpc1    byskycell warp     null   MD01   skycell.069  null   g.00000    0      0    | PS1-0811001 g 
+0         36.945000     -4.579750          250   250      2        stamp       1        gpc1    byskycell warp     null   MD01   skycell.069  null   r.00000    0      0    | PS1-0811001 r
+0         36.945000     -4.579750          250   250      2        stamp       1        gpc1    byskycell warp     null   MD01   skycell.069  null   i.00000    0      0    | PS1-0811001 i
+0         36.945000     -4.579750          250   250      2        stamp       1        gpc1    byskycell warp     null   MD01   skycell.069  null   z.00000    0      0    | PS1-0811001 z
+0         36.945000     -4.579750          250   250      2        stamp       1        gpc1    byskycell warp     null   MD01   skycell.069  null   z.00000    0      0    | PS1-0811001 y
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/ps1-0905001-all.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/ps1-0905001-all.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/ps1-0905001-all.txt	(revision 41103)
@@ -0,0 +1,46 @@
+# Sample Postage stamp request description file
+# This can be parsed by the program pstamp_request_file to build a PS1_PSTAMP_REQUEST binary table
+
+# First line of data is for the extension header
+# The order of keywords follows TABLE 6 of ICD
+#
+# Note that value for REQ_NAME may be overriden by a command line
+# argument to pstamp_request_create.
+# Blank and comment lines are ignored
+
+# REQ_NAME      EXTVER
+CHANGEME          1
+
+# subsequent lines define the rows in the table
+
+
+# ID    |     ROI Specification                   |  JOB Specification | Images of interest specification
+# ROWNUM CENTER_X         CENTER_Y         WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT REQ_TYPE IMG_TYPE    ID   TESS_ID COMPONENT DIST_GROUP     REQFILT MJD_MIN MJD_MAX | COMMENT
+#
+# various looks at the location of SN candidate PS1-0905001
+1         243.035580     55.128140          250   250      2        stamp       63       gpc1    byid     raw   70853         null   null       null        null    0      0    |PS1-0905001 r
+2         243.035580     55.128140          250   250      2        stamp       63       gpc1    byid     chip  20272         null   null       null        null    0      0    |PS1-0905001 r
+
+# 3 and 4 will fail because the weight images was damaged by the destreaking
+# process
+3         243.035580     55.128140          250   250      2        stamp       63       gpc1    byid     warp  6323          null   null       null        null    0      0    |PS1-0905001 r
+4         243.035580     55.128140          250   250      2        stamp       63       gpc1    byid     diff  16880         null   null       null        null    0      0    |PS1-0905001 r
+# these will work
+103       243.035580     55.128140          250   250      2        stamp       59       gpc1    byid     warp  6323          null   null       null        null    0      0    |PS1-0905001 r
+104       243.035580     55.128140          250   250      2        stamp       59       gpc1    byid     diff  16880         null   null       null        null    0      0    |PS1-0905001 r
+
+5         243.035580     55.128140          250   250      2        stamp       63       gpc1    byid     stack 14032         null   null       null        null    0      0    |PS1-0905001 r
+
+6         243.035580     55.128140          250   250      2        stamp       1        gpc1    byexp    raw   o4973g0133o   null   null       null        null    0      0    |PS1-0905001 r
+7         243.035580     55.128140          250   250      2        stamp       1        gpc1    byexp    chip  o4973g0133o   null   null  MD08.2009110     null    0      0    |PS1-0905001 r
+8         243.035580     55.128140          250   250      2        stamp       1        gpc1    byexp    warp  o4973g0133o   null   null  MD08.2009110     null    0      0    |PS1-0905001 r
+9         243.035580     55.128140          250   250      2        stamp       1        gpc1    byexp    diff  o4973g0133o   null   null  MD08.2009110     null    0      0    |PS1-0905001 r
+
+
+# this should find warp 6323 givin the time cuts
+10        243.035580     55.128140          250   250      2        stamp       1        gpc1    bycoord  warp  null          null   null  MD08.200905.v1   null  54973.53543 54973.5368 |PS1-0905001 r
+
+11        243.035580     55.128140          250   250      2        stamp       1        gpc1    bydiff   chip  194350        null   null      null         null    0      0    |PS1-0905001 r
+12        243.035580     55.128140          250   250      2        stamp       1        gpc1    bydiff   warp  194350        null   null      null         null    0      0    |PS1-0905001 r
+13        243.035580     55.128140          250   250      2        stamp       1        gpc1    bydiff   diff  194350        null   null      null         null    0      0    |PS1-0905001 r
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/ps1-0905001-diff.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/ps1-0905001-diff.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/ps1-0905001-diff.txt	(revision 41103)
@@ -0,0 +1,22 @@
+# Sample Postage stamp request description file
+# This can be parsed by the program pstamp_request_file to build a PS1_PSTAMP_REQUEST binary table
+
+# First line of data is for the extension header
+# The order of keywords follows TABLE 6 of ICD
+#
+# Note that value for REQ_NAME may be overriden by a command line
+# argument to pstamp_request_create.
+# Blank and comment lines are ignored
+
+# REQ_NAME      EXTVER
+byskycell_test     1
+
+# subsequent lines define the rows in the table
+
+# If ROWNUM is set to zero, pstamp_request_file will set insert a value for each row beginning with 1
+# If a later input row reuses a value an error is preported
+
+# ID    |     ROI Specification                   |  JOB Specification | Images of interest specification
+# ROWNUM CENTER_X         CENTER_Y         WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT REQ_TYPE IMG_TYPE    ID   TESS_ID COMPONENT    LABEL  REQFILT MJD_MIN MJD_MAX | COMMENT
+#
+1         243.035580     55.128140          250   250      2        stamp       1        gpc1    byskycell diff     null   MD08   skycell.078  null   r.00000  54972    55000 | PS1-0905001 r
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/ps1-0905001-warp.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/ps1-0905001-warp.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/ps1-0905001-warp.txt	(revision 41103)
@@ -0,0 +1,26 @@
+# Sample Postage stamp request description file
+# This can be parsed by the program pstamp_request_file to build a PS1_PSTAMP_REQUEST binary table
+
+# First line of data is for the extension header
+# The order of keywords follows TABLE 6 of ICD
+#
+# Note that value for REQ_NAME may be overriden by a command line
+# argument to pstamp_request_create.
+# Blank and comment lines are ignored
+
+# REQ_NAME      EXTVER
+byskycell_test     1
+
+# subsequent lines define the rows in the table
+
+# If ROWNUM is set to zero, pstamp_request_file will set insert a value for each row beginning with 1
+# If a later input row reuses a value an error is preported
+
+# ID    |     ROI Specification                   |  JOB Specification | Images of interest specification
+# ROWNUM CENTER_X         CENTER_Y         WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT REQ_TYPE IMG_TYPE    ID   TESS_ID COMPONENT    LABEL  REQFILT MJD_MIN MJD_MAX | COMMENT
+#
+0         243.035580     55.128140          250   250      2        stamp       1        gpc1    byskycell warp     null   MD08   skycell.078  null   g.00000    0      0    | PS1-0905001 g 
+0         243.035580     55.128140          250   250      2        stamp       1        gpc1    byskycell warp     null   MD08   skycell.078  null   r.00000    0      0    | PS1-0905001 r
+0         243.035580     55.128140          250   250      2        stamp       1        gpc1    byskycell warp     null   MD08   skycell.078  null   i.00000    0      0    | PS1-0905001 i
+0         243.035580     55.128140          250   250      2        stamp       1        gpc1    byskycell warp     null   MD08   skycell.078  null   z.00000    0      0    | PS1-0905001 z
+0         243.035580     55.128140          250   250      2        stamp       1        gpc1    byskycell warp     null   MD08   skycell.078  null   z.00000    0      0    | PS1-0905001 y
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/ssdiff.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/ssdiff.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/ssdiff.txt	(revision 41103)
@@ -0,0 +1,31 @@
+# Sample Postage stamp request description file
+# This can be parsed by the program pstamp_req_create to build a 
+# PS1_PSTAMP_REQUEST binary table
+
+# First line of data is for the extension header
+# The order of keywords follows TABLE 6 of ICD
+#
+# Note that value for REQ_NAME may be overriden by a command line
+# argument to pstamp_req_create.
+# Blank and comment lines are ignored
+
+# REQ_NAME      EXTVER
+CHANGEME         1
+
+# subsequent lines define the rows in the table
+
+# If ROWNUM is set to zero, pstamp_request_file will set insert a value for each row beginning with 1
+# If a later input row reuses a value an error occurs
+
+#
+# ID    |     ROI Specification                   |  JOB Specification | Images of interest specification
+# ROWNUM CENTER_X    CENTER_Y  WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT REQ_TYPE IMG_TYPE    ID   TESS_ID     COMPONENT    DATA_GROUP  REQFILT MJD_MIN MJD_MAX | COMMENT
+#
+0       160.29297   58.2564    200    200      2        stamp         1       gpc1    byid     diff     41501    null      null             null     null      0     0       |a ssdiff
+0       160.29297   58.2564    200    200      2        stamp         1       gpc1    bydiff   diff    1868781    null      null             null     null      0     0      |a ssdiff
+0       160.29297   58.2564    200    200      2        stamp         1       gpc1    bydiff   stack   1868781    null      null             null     null      0     0      |a ssdiff
+0       160.29297   58.2564    200    200      2        stamp        1025     gpc1    bydiff   stack   1868781    null      null             null     null      0     0      |a ssdiff get template
+0       160.29297   58.2564    200    200      2        stamp        2049     gpc1    bydiff   stack   1868781    null      null             null     null      0     0      |a ssdiff unconvolved
+0       160.29297   58.2564    200    200      2        stamp        3073     gpc1    bydiff   stack   1868781    null      null             null     null      0     0      |a ssdiff get unconvolved template
+# this will generate an error
+0       160.29297   58.2564    200    200      2        stamp        1025     gpc1    bydiff   warp   1868781    null      null             null     null      0     0      |a ssdiff get template
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/test1.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/test1.txt	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/test1.txt	(revision 41103)
@@ -0,0 +1,28 @@
+# test1.txt
+# A sample Postage Stamp request description file.
+# This can be parsed by the program pstamp_req_create to build a PS1_PSTAMP_REQUEST fits binary table
+
+# First line of data is for the extension header
+#
+# Note that value for REQ_NAME may be overriden by the pstamp_req_create command line argument --req_name
+# Blank and comment lines (like this one) are ignored
+
+# REQ_NAME      EXTVER
+test1            1
+
+# subsequent lines define the rows in the table
+
+# If ROWNUM is set to zero, pstamp_request_file will set insert a value for each row beginning with 1
+# If a later input row reuses a value an error occurs
+
+#
+# ID    |     ROI Specification                   |  JOB Specification | Images of interest specification
+# ROWNUM CENTER_X    CENTER_Y  WIDTH HEIGHT COORD_MASK JOB_TYPE OPTION_MASK PROJECT REQ_TYPE IMG_TYPE    ID   TESS_ID COMPONENT    DATA_GROUP  REQFILT MJD_MIN MJD_MAX | COMMENT
+#
+0       135.49921     34.92035 100    100      2        stamp       1        gpc1    bycoord   chip       null    null   null           null        null 55249   55250   |search by coords (make the server work)
+# just change the coordinates for the next request
+0       135.63231     34.92165 100    100      2  | different coords in same images of interest
+0       135.49921     34.92035 100    100      2        stamp       1        gpc1    byid     chip       62326    null   XY62           null        null   0       0     |specific image
+0       135.49921     34.92035 100    100      2        stamp       3        gpc1    byid     chip       62326    null   XY62           null        null   0       0     |image,mask,and weight
+0       135.49921     34.92035 100    100      2        stamp      63        gpc1    byid     chip       62326    null   XY62           null        null   0       0     |image,mask,weight,cmf, psf,backmdl
+0       135.49921     34.92035 100    100      2        stamp       1        gpc1    bycoord   chip       null    null   null           null        null 55249   55250   |search by coords (make the server work)
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/test_lookup.pl
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/test_lookup.pl	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/test_lookup.pl	(revision 41103)
@@ -0,0 +1,85 @@
+#!/bin/env perl
+###
+###     Tests for the PStamp module
+###
+
+use warnings;
+use strict;
+
+
+use Getopt::Long qw( GetOptions );
+use PS::IPP::PStamp::Job qw( :standard );
+
+use PS::IPP::Config qw( :standard );
+my $ipprc = PS::IPP::Config->new(); # IPP Configuration
+
+my $verbose;
+my $image_db;
+my $req_type;
+my $img_type;
+my $id;
+my $x;
+my $y;
+my $mjd_min;
+my $mjd_max;
+my $filter;
+my $skycell_id;
+my $tess_id;
+my $component;
+my $data_group;
+my $inverse;
+my $need_magic = 1;
+
+
+
+GetOptions(
+    'dbname=s'  =>  \$image_db,
+    'req_type=s'=>  \$req_type,
+    'img_type=s'=>  \$img_type,
+    'id=s'      =>  \$id,
+    'component=s'=>  \$component,
+    'x=s'       =>  \$x,
+    'y=s'       =>  \$y,
+    'mjd_min=s' =>  \$mjd_min,
+    'mjd_max=s' =>  \$mjd_max,
+    'filter=s'  =>  \$filter,
+    'verbose'   =>  \$verbose,
+);
+
+my $err = "";
+$err .= " --req_type is required" if !$req_type;
+$err .= " --img_type is required" if !$img_type;
+$err .= " --id is required" if ($req_type ne "bycoord") && !$id;
+$err .= " --dbname is required" if !$image_db;
+
+if ($err) {
+    print STDERR "$err\n";
+    exit $PS_EXIT_DATA_ERROR;
+}
+
+$component = "null" if !defined($component);
+
+my $results = locate_images($ipprc, $image_db, $req_type, $img_type, $id, $tess_id, $component,
+            $inverse, $need_magic, 
+            $x, $y, $mjd_min, $mjd_max, $filter, $data_group, $verbose);
+
+foreach my $i (@$results) {
+    print "${img_type}_id: $id" if $id;
+    if ($i->{exp_id}) {
+        print " exp_id $i->{exp_id}\n"
+    } else {
+        print "\n";
+    }
+    print "\timage:  $i->{image}\n";
+    print "\tmask:   $i->{mask}\n"   if $i->{mask};
+    print "\tweight: $i->{weight}\n" if $i->{weight};
+    print "\tastrom: $i->{astrom}\n" if $i->{astrom};
+    if ($img_type ne "stack") {
+        print "\tcamera: $i->{camera}\n";
+        print "\texp_id: $i->{exp_id}\n";
+        print "\texp_name: $i->{exp_name}\n";
+    }
+}
+
+exit 0;
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/test/testrequest
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/test/testrequest	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/test/testrequest	(revision 41103)
@@ -0,0 +1,183 @@
+#!/bin/env  perl
+
+# program to create a sample detectability query or postage stamp request file
+# and register it in a Data Store
+
+#  default products for local configuration
+
+my $dquery_product = "detectability_query";
+my $pstamp_product = "pstamprequests";
+
+# note: dsreg also needs IPP site configuration to connect to the database
+
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version gnu_getopt );
+use Pod::Usage qw( pod2usage );
+use IPC::Cmd 0.36 qw( can_run run );
+use File::Temp qw(tempfile);
+
+
+my ($query_id, $req_name, $product, $input, $fs_name, $verbose, $save_temps);
+
+GetOptions(
+    'input|i=s'     => \$input,
+    'query_id|q=s'  => \$query_id,
+    'req_name|r=s'  => \$req_name,
+    'verbose|v'     => \$verbose,
+    'save-temps'    => \$save_temps,
+    'dbname|d=s'    => \$dbname,
+    'product|p=s'   => \$product,
+) or pod2usage( 2 );
+
+die "--query_id or --req_name is required\n" if ((!$query_id) && (!$req_name));
+die "only one of --query_id or --req_name is allowed\n" if ($query_id && $req_name);
+
+my $dsreg = can_run("dsreg") or die "cant'f find dsreg";
+my $detect_query_create = can_run("detect_query_create") or die "cant'f find detect_query_create";
+my $pstamp_req_create = can_run("pstamp_req_create") or die "cant'f find pstamp_req_create";
+
+my $fileset_type;
+my $create_cmd;
+
+# if query_id is supplied we make a detectability query
+# else req_name was supplied and we make a postage stamp request
+if ($query_id) {
+    $create_prog = $detect_query_create;
+    $req_name = $query_id;
+    $product = $dquery_product if !$product;
+    $fileset_type = "MOPS_DETECTABILITY_QUERY";
+    $create_cmd = "$detect_query_create --query_id $query_id";
+} else {
+    die "must supply input request decription file\n" if !$input;
+    $product = $pstamp_product if !$product;
+    $fileset_type = "PSREQUEST";
+    $create_cmd = "$pstamp_req_create --req_name $req_name";
+}
+$fs_name = $req_name;
+
+# request fits table
+my $req_file;
+my $RFH;
+my $datapath;
+if ($save_temps) {
+    $req_file = "${req_name}.fits";
+    $datapath = ".";
+} else {
+    ($RFH, $req_file) = tempfile("/tmp/${req_name}.fits.XXXX", UNLINK => 1);
+    die "failed to create tempfile" if !$RFH;
+
+    $datapath = "/";
+}
+
+
+my $query_txt_file;
+my $FH;
+# if no query file is supplied make one from the Here document in the subroutine make_default_query
+if ($input) {
+    $query_txt_file = $input;
+} else {
+    ($FH, $query_txt_file) = tempfile("/tmp/dquery.txt.XXXX", UNLINK => !$save_temps);
+    die "failed to create tempfile" if !$FH;
+    make_default_dquery($FH);
+}
+
+die "can't find request description file $query_txt_file" if (!-e $query_txt_file);
+
+$create_cmd .= " --input $query_txt_file --output $req_file";
+
+my $reg_cmd = "echo '$req_file|||table|'"
+            . " | $dsreg --product $product --type $fileset_type"
+            . " --add $fs_name --copy --datapath $datapath --list -"
+            . " --dbname $dbname" if $dbname;
+
+{
+    my $command = "$create_cmd";
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        print STDERR "command failed error code: $error_code\nstderr:\n\t";
+        print STDERR @$stderr_buf;
+        print "\n";
+
+        my $exit_status = $error_code >> 8;
+        print STDERR "exiting with status $exit_status\n" if $verbose;
+        exit $exit_status;
+    }
+}
+{
+    my $command = "$reg_cmd";
+    my ( $success, $error_code, $full_buf, $stdout_buf, $stderr_buf ) =
+        run(command => $command, verbose => $verbose);
+    unless ($success) {
+        print STDERR "command failed error code: $error_code\nstderr:\n\t"
+            if $verbose;
+        print STDERR @$stderr_buf;
+        print "\n" if $verbose;
+
+        my $exit_status = $error_code >> 8;
+        print STDERR "exiting with status $exit_status\n" if $verbose;
+        exit $exit_status;
+    }
+}
+exit 0;
+
+sub make_default_dquery()
+{
+    my $FH = shift;
+    my $data = <<'THE_END';
+# Sample data
+# First line of data is for the HEADER
+# order of keywords follows TABLE 6 of ICD
+# QUERY_ID EXTVER FPA_ID       MJD-OBS FILTER OBSCODE
+QUERY42       1   o4608g0103o   54608     g     566
+#
+# ROWNUM     RA1_DEG        DEC1_DEG    RA2_DEG     DEC2_DEG     MAG
+1            312.44049389 30.54022727  312.44051968 30.54024139  18.4228     
+2            313.03337881 31.01317194  313.03344194 31.01324268  18.3961     
+3            312.91159232 30.95195459  312.91153476 30.95190113  18.1110     
+4            312.26742527 30.95207284  312.26744769 30.95206300  17.0908     
+5            313.20263734 30.62317841  313.20266984 30.62310935  18.2890     
+6            312.85365023 30.45667552  312.85358645 30.45661659  16.9252     
+7            312.86257534 30.73005531  312.86264564 30.73012644  16.5870     
+8            312.62651492 30.28398046  312.62654595 30.28390213  16.6911     
+9            312.30703743 30.99927709  312.30708709 30.99927981  17.4722     
+10           312.95440987 30.21452885  312.95434880 30.21444691  18.5156     
+11           313.09947609 30.93800421  313.09940219 30.93795549  17.8855     
+12           312.97417836 31.11301952  312.97423473 31.11296140  16.9911     
+13           313.11781918 30.37359317  313.11784695 30.37360958  16.7468     
+14           312.76927925 30.53972354  312.76933224 30.53976126  16.8949     
+15           313.14717536 30.25958068  313.14710913 30.25951218  16.8300     
+16           312.30604633 30.88958513  312.30599425 30.88959415  18.2257     
+17           312.43565662 30.43038293  312.43560908 30.43037934  19.0005     
+18           312.55589195 30.81649562  312.55594584 30.81655133  18.6992     
+19           312.48925222 30.79092013  312.48924050 30.79087193  19.2099     
+20           312.94560207 31.15170816  312.94554641 31.15162988  17.2162     
+21           313.19589134 30.74674862  313.19594142 30.74682054  16.6793     
+22           312.70278940 30.52926292  312.70271911 30.52926109  19.0436     
+23           312.56015422 30.77354938  312.56021167 30.77360353  19.2750     
+24           312.87989916 30.78926857  312.87996776 30.78920032  18.2502     
+25           312.81101394 30.65212801  312.81096654 30.65215390  18.5487     
+26           312.55607541 30.23260894  312.55612602 30.23263745  17.2404     
+27           312.47366234 31.02186968  312.47372919 31.02192895  18.9959     
+28           312.90062087 31.13704835  312.90061590 31.13697785  17.5652     
+29           313.01177949 31.16629118  313.01181621 31.16626516  17.0270     
+30           312.40371494 30.98672316  312.40366292 30.98677392  19.1234     
+31           312.56662477 31.01082034  312.56665651 31.01078413  17.6238     
+32           312.61398923 30.64616956  312.61399907 30.64618259  18.5522     
+33           313.07268041 30.96857880  313.07272131 30.96859005  17.7002     
+34           312.36144521 30.43367984  312.36146205 30.43373589  16.6557     
+35           312.74549146 30.89623741  312.74554546 30.89630365  18.8037     
+36           312.49256362 30.78465056  312.49260740 30.78459825  16.7909     
+37           312.92641571 30.76107785  312.92634812 30.76111408  17.2734     
+38           312.84561663 30.38510072  312.84561100 30.38515538  18.3349     
+39           312.48297202 31.08290934  312.48297374 31.08290383  17.6822     
+40           312.27670734 30.89704117  312.27664961 30.89698871  17.4518     
+41           312.82856967 30.65880493  312.82859245 30.65878537  18.8543     
+42           313.05680974 30.97207965  313.05672748 30.97211785  16.6627     
+THE_END
+
+    print $FH $data;
+
+    close $FH;
+}
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/web/authenticate.php
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/web/authenticate.php	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/web/authenticate.php	(revision 41103)
@@ -0,0 +1,33 @@
+<?php
+
+// function check_login()
+
+session_start();
+
+// XXX TODO use mysql
+$auth_user="setme";
+$auth_passwd="setmetoo";
+
+$user   = $_SERVER['PHP_AUTH_USER'];
+$passwd = $_SERVER['PHP_AUTH_PW'];
+$did_login = isset($_SESSION['did_login']);
+
+if ($did_login && isset($user) && isset($passwd) &&
+    ($auth_user == $user) && ($auth_passwd == $passwd)) {
+
+    echo "Welcome:  " . $user;
+    echo "&nbsp;&nbsp;&nbsp; <a href=\"./logout.php\">Logout</a>";
+    echo "<br />";
+    echo "<br />";
+} else {
+    $_SESSION['did_login'] = true;
+    header('WWW-Authenticate: Basic realm="Restricted Section"');
+    header('HTTP/1.0 401 Unauthorized');
+    // The following will be output if the user hits the cancel button
+    echo "please enter username and password";
+    echo "&nbsp;&nbsp;&nbsp;<a href=\"./upload.php\">Login</a>";
+    exit;
+}
+
+?>
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/web/logout.php
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/web/logout.php	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/web/logout.php	(revision 41103)
@@ -0,0 +1,18 @@
+<?php
+
+// require_once "authenticate.php";
+
+// logout();
+session_start();
+$_SESSION = array();
+if (isset($_COOKIE[session_name()])) {
+    setcookie(session_name(), '', time()-42000, '/');
+}
+session_destroy();
+
+echo "You are now logged out<br /><br />";
+echo "<a href=\"./upload.php\">Upload</a>";
+
+// phpinfo(32);
+?>
+
Index: /branches/ccl_branches/ipponly-20191108/pstamp/web/pstamp.php
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/web/pstamp.php	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/web/pstamp.php	(revision 41103)
@@ -0,0 +1,6 @@
+<?php
+
+require_once "pstampconfig.php";
+require_once "authenticate.php";
+
+?>
Index: /branches/ccl_branches/ipponly-20191108/pstamp/web/pstampconfig.php
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/web/pstampconfig.php	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/web/pstampconfig.php	(revision 41103)
@@ -0,0 +1,19 @@
+<?php
+// BEGIN Local configuration
+
+$WORKDIR = "set to the location of pstamp working directory";
+$dsroot = "set to the root of the data store";
+$dbname = "set to the pstamp database name";
+$dbserver = "set to the pstamp database server";
+
+$PSCONFDIR = "";
+$PSCONFIG  = "";
+$PSBINDIR  = "$PSCONFDIR/$PSCONFIG.lin64/bin";
+
+// END Local configuration 
+
+# this script sets up the environment to run IPP commands with current directory
+# $WORKDIR
+$SCRIPT    = "$PSBINDIR/pstamp_runcommand.sh $PSCONFDIR $PSCONFIG $WORKDIR";
+
+?>
Index: /branches/ccl_branches/ipponly-20191108/pstamp/web/request.php
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/web/request.php	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/web/request.php	(revision 41103)
@@ -0,0 +1,760 @@
+<?php 
+
+// prototype postage stamp server web interface
+
+// A php program that generates a postage stamp request form
+
+// There are two modes on the form List Images simply lists the input images
+// that match the image selection critera
+
+// When make stamps is selected when the page is posted, a postage stamp request is queued.
+// 
+// Then we respond to http posts by listing the status of the jobs.
+// Once all of the jobs complete we set the page back to submission mode again
+// The mode for the page is given by $request_id != 0
+
+// XXX This is just a prototype for testing purposes. 
+
+require "pstamp.php";
+require "submitted.php";
+
+# this script sets up the environment to run IPP commands with current directory
+# $WORKDIR
+$SCRIPT    = "$PSBINDIR/pstamp_runcommand.sh $PSCONFDIR $PSCONFIG $WORKDIR";
+
+// END of moved to pstampconfig.php
+
+// Initialize variables
+$output_array = array();
+
+$request_id = 0;
+$last_request_id = 0;
+
+$raw_selected = "";
+$chip_selected = "";
+$warp_selected = "";
+$stack_selected = "";
+$diff_selected = "";
+
+$exp_checked = "";
+$file_checked = "";
+$coord_checked = "";
+$diff_checked = "";
+$list_checked = "";
+$getstatus_checked = "";
+$pstamp_checked = "";
+$get_checked = "";
+
+$command_line = "";
+$error_line = "";
+$command_status = "";
+
+$outFileset = "";
+
+// Initialize the request variables that we depend upon
+$rvar_project="";
+
+$gpc1_selected="";
+$mops_selected="";
+$simtest_selected="";
+
+$require_class_id = 0;
+
+$rvar_select_by = "";
+$rvar_img_type = "";
+$rvar_id = "";
+$rvar_class_id = "";
+$rvar_cell_id = "";
+
+$rvar_center_type = "";
+$rvar_range_type  = "";
+
+$rvar_RA = "";
+$rvar_DEC = "";
+$rvar_dRA = "";
+$rvar_dDEC = "";
+$rvar_X = "";
+$rvar_Y = "";
+$rvar_W = "";
+$rvar_H = "";
+
+$rvar_cmd_mode = "";
+$rvar_last_cmd_mode = "";
+$rvar_request_id = 0;
+$rvar_last_request_id = 0;
+
+// now get the values from this post
+
+import_request_variables("gp", "rvar_");
+
+if ($rvar_project == "gpc1") {
+    $gpc1_selected = "selected";
+//    $require_class_id = 1;
+} else if ($rvar_project == "megacam-mops") {
+    $mops_selected = "selected";
+    $require_class_id = 0;
+} else { //    if ($rvar_project == "simtest") {
+    $simtest_selected = "selected";
+    $require_class_id = 0;
+}
+
+// figure out which select_by is set and save it's checked value 
+
+if ($rvar_select_by == "exposure_id") {
+    $exp_checked = "CHECKED";
+} else if ($rvar_select_by == "db_id") {
+    $file_checked = "CHECKED";
+} else if ($rvar_select_by == "coord") {
+    $coord_checked = "CHECKED";
+} else if ($rvar_select_by == "diff_image_id") {
+    $diff_checked = "CHECKED";
+} else {
+    // nothing checked default to By ID
+    $file_checked = "CHECKED";
+}
+
+// get the Image type
+if ($rvar_img_type == "raw") {
+    $raw_selected = "selected";
+} else if ($rvar_img_type == "chip") {
+    $chip_selected = "selected";
+} else if ($rvar_img_type == "warp") {
+    $warp_selected = "selected";
+} else if ($rvar_img_type == "stack") {
+    $stack_selected = "selected";
+} else if ($rvar_img_type == "diff") {
+    $diff_selected = "selected";
+}
+
+// is the center is specified in Pixels or sky coordinates
+if ($rvar_center_type == "Pixels") {
+    $sky_checked="";
+    $pix_checked="checked";
+} else {
+    $sky_checked="checked";
+    $pix_checked="";
+}
+
+// is the range is specified in Pixels or sky coordinates
+if ($rvar_range_type == "Sky") {
+    $rsky_checked="checked";
+    $rpix_checked="";
+} else {
+    $rpix_checked="checked";
+    $rsky_checked="";
+}
+
+// When request_id is non-zero we respond to posts by check the status of that request
+// request_id gets set to zero when the status of all jobs for the request is 'stop'
+$request_id = $rvar_request_id;
+$last_request_id = $rvar_last_request_id;
+
+// during request the page doesn't have cmd_mode set. So we remember the lat real value
+// and use that
+if (!$rvar_cmd_mode) {
+    $rvar_cmd_mode = "$rvar_last_cmd_mode";
+}
+
+$getstatus_checked = "";
+$pstamp_checked = "";
+$list_checked = "";
+$get_checked = "";
+if ($rvar_cmd_mode == "Make Stamps") {
+    $pstamp_checked = "checked";
+} else if ($rvar_cmd_mode == "Get Images") {
+    $get_checked = "checked";
+} else if ($rvar_cmd_mode == "Get Status") {
+    $getstatus_checked = "checked";
+} else {
+    // default
+    $pstamp_checked = "checked";
+}
+
+
+// echo "rvar_request_id: $rvar_request_id\n";
+
+// HERE is the logic for running the various commands
+
+// How do we know whether or not this is the intial page load or not?
+// Well, the first time rvar_img_type is not set. So we key off of that.
+// TODO: find a better way to decide whether or not to run commands
+
+if ($rvar_img_type) {
+    $jobFinished = 0;
+    if (! $getstatus_checked) {
+        try {
+            $command_line = build_request_cmd();
+            $error_line = "";
+            run_command($command_line);
+            if (! $list_checked) {
+                // The only output from a successful run is the request_id
+                $request_name = trim(Array_pop($output_array));
+                $request_id = Array_pop($output_array);
+                $last_request_id = $request_id;
+                if ($request_id && $request_name) {
+                    addRequest($request_id, $request_name);
+                    // setcookie("our_request_id", $request_id);
+                    // echo "The request id is $request_id\n";
+                    $getstatus_checked = "checked";
+                    $pstamp_checked = "";
+                } else {
+                    // XXX: TODO print out the error
+                    if (count($output_array) != 0) {
+                        throw new Exception("unexpected output returned by pstampwebrequest.");
+                    }
+                }
+            } else {
+                $last_request_id = 0;
+            }
+        } catch (Exception $e) {
+            $error_line = $e->getMessage();
+        }
+    }
+}
+
+// This is the end of the Logic
+
+function build_request_cmd()
+{
+    global $rvar_project;
+    global $sky_checked, $rsky_checked;
+    global $list_checked;
+    global $get_checked;
+    global $rvar_RA, $rvar_DEC;
+    global $rvar_dRA, $rvar_dDEC;
+    global $rvar_X, $rvar_Y;
+    global $rvar_W, $rvar_H;
+    global $exp_checked, $file_checked, $coord_checked, $diff_checked;
+    global $rvar_img_type;
+    global $rvar_id, $rvar_class_id;
+    global $command_line;
+    global $dbname;
+    global $dbserver;
+    global $require_class_id;
+    global $PSCONFDIR, $PSCONFIG, $WORKDIR;
+    global $SCRIPT;
+
+    $making_stamps = 1;
+    $cmd = "$SCRIPT pstamp_webrequest.pl";
+
+    if ($list_checked) {
+        $cmd .= " --job_type list_uri";
+        $making_stamps = 0;
+    } else if ($get_checked) {
+        $making_stamps = 0;
+        $cmd .= " --job_type get_image";
+    }
+
+    if ($dbname) {
+        $cmd .= " --dbname $dbname --dbserver $dbserver";
+    }
+
+    if (! $rvar_project ) {
+        throw new Exception('project must be specified.');
+    }
+    $cmd .= " --project $rvar_project";
+
+    if ($making_stamps) {
+        // TODO: put options on the GUI for these
+//        $cmd .= " -mask -weight";
+    }
+
+    if ($making_stamps || $coord_checked) {
+        // Set up the ROI parameters
+        if ($sky_checked) {
+            if (! $rvar_RA || ! $rvar_DEC) {
+                throw new Exception('RA and DEC must be specified.');
+            }
+            $cmd .= " --ra $rvar_RA --dec $rvar_DEC";
+        } else {
+            if (! $rvar_X || ! $rvar_Y) {
+                throw new Exception('X and Y must be specified.');
+            }
+            $cmd .= " -pixcenter --x $rvar_X --y $rvar_Y";
+        }
+
+        if ($rsky_checked) {
+            if (! $rvar_dRA || ! $rvar_dDEC) {
+                throw new Exception('dRA and dDEC must be specified.');
+            }
+            $cmd .= " --arcseconds --width $rvar_dRA --height $rvar_dDEC";
+        } else {
+            if (! $rvar_W || ! $rvar_H) {
+                throw new Exception('width and height must be specified.');
+            }
+            $cmd .= " --width $rvar_W --height $rvar_H";
+        }
+    }
+
+
+    if (! $rvar_img_type) {
+        // this actually can't happen
+        throw new Exception('Must set image type.');
+    }
+
+    if ($exp_checked) {
+        if ($rvar_img_type == "stack") {
+            throw new Exception('Lookup by exposure name not supported for stack images.');
+        }
+        if (! $rvar_id ) {
+            throw new Exception('Must set ID to the Exposure ID.');
+        }
+        $cmd .= " --req_type byexp --stage $rvar_img_type --id $rvar_id";
+    } else if ($file_checked) {
+        if (! $rvar_id ) {
+            throw new Exception('Must set ID to the exposure name.');
+        }
+        $cmd .= " --req_type byid --stage $rvar_img_type --id $rvar_id";
+    } else if ($coord_checked) {
+        $cmd .= " --req_type bycoord --stage $rvar_img_type";
+        $coord_checked = "checked";
+//        throw new Exception("Image selection by coordinate not implemented yet.");
+    } else if ($diff_checked) {
+        if (! $rvar_id ) {
+            throw new Exception('Must set ID to Diff Image ID.');
+        }
+        $cmd .= " --req_type bydiff --stage $rvar_img_type --id $rvar_id";
+    }
+
+// XXX: don't need to require class_id anymore
+//    if (($rvar_img_type == "raw") || ($rvar_img_type == "chip")) {
+//        if (!$sky_checked && ($require_class_id && ! $rvar_class_id )) {
+//            throw new Exception("must specify Class ID with Image Type $rvar_img_type.");
+//        }
+        // leave off compoennt if we're looking up by coordinates. It breaks it
+        if (!$coord_checked && (($rvar_class_id) && ($rvar_class_id != "all"))) {
+            $cmd .= " --component $rvar_class_id";
+        }
+//    }
+
+    return escapeshellcmd($cmd);
+}
+
+function run_command($command_line)
+{
+    global $output_array;
+    global $error_line;
+    global $command_status;
+
+    //    echo "running $command_line\n";
+
+    exec ("$command_line", $output_array, $command_status);
+
+    $size = sizeof($output_array);
+    //     echo "command_status: $command_status output_array  contains $size lines\n";
+    if ($command_status == 0) {
+        // On success we just remember the results
+        $dump_results = 0;
+        if ($dump_results) {
+            echo "Output: $size lines\n";
+            for ($i = 0; $i < $size; $i++) {
+                echo "$output_array[$i]\n";
+            }
+        }
+    } else {
+        // copy the output to the error_line
+        $error_line = "";
+        for ($i = 0; $i < $size; $i++) {
+             $error_line .= "$output_array[$i]\n";
+        }
+    }
+
+}
+
+// This is no longer used
+function printURL($line)
+{
+    global $request_id;
+    global $last_request_id;
+
+    echo "<tr><td>";
+    $doURL = 1;
+    if ($doURL) {
+// echo "<pre>output_line: $line\n</pre>";
+        // Parse the output from pstamp_list_jobs.pl
+        $elements = explode(" ", $line);
+        if (count($elements) == 6) {
+            $job_id   = $elements[0];
+            $state    = $elements[1];
+            $fault    = $elements[2];
+            $req_name = $elements[3];
+            $product  = $elements[4];
+            $path     = $elements[5];
+            $fileName = basename($path);
+            if ($state == "stop") {
+                global $dsroot;
+                $dirName  = "$dsroot/$product/$req_name";
+                // XXX: TODO: make this a configuration parameter
+                $filesetURL = "http://datastore.ipp.ifa.hawaii.edu/$product/$req_name";
+                $fullpath = "$dirName/$fileName";
+// echo "<pre>fullpath: $fullpath filesetURL: $filesetURL\n</pre>";
+                if (file_exists($dirName)) {
+                    // this job is done, list the url as a link
+                    // echo "<a href=\"http:$path\" target=\"_blank\" type=\"image/fits\">";
+                    echo "<a href=\"$filesetURL\" TARGET=\"form_results_fileset\">";
+#                    echo $fileName;
+                    $filesetName = basename($dirName);
+                    echo "Fileset: $filesetName";
+                    echo "</a>";
+                    echo "&nbsp;&nbsp;&nbsp; Base: $fileName &nbsp;&nbsp;&nbsp; request id: $last_request_id &nbsp;&nbsp;";
+                    echo "job_id: $job_id &nbsp;&nbsp;&nbsp; state: $state";
+                } else {
+                    echo "request id: $last_request_id  job id: $job_id failed";
+                    echo "   $fullpath";
+                }
+            } else {
+                // TODO: refine this output
+                echo "$fileName&nbsp;&nbsp;&nbsp; request_id: $request_id &nbsp;&nbsp;&nbsp;";
+                echo "job_id: $job_id &nbsp;&nbsp;&nbsp; state: $state";
+            }
+        }
+    } else {
+        print "$line";
+    }
+
+    echo "</td></tr>";
+}
+?>
+
+<!----------------------Beginning of the HTML --------------------------------------------- -->
+
+<html>
+<head>
+  <title>
+    Postage Stamp Request Form (prototype)
+  </title>
+</head>
+<body>
+
+<H1 align=center>
+Postage Stamp Request Form
+</h1>
+
+<?php
+    welcomeHeader($auth_user, "pstamp_links.php", "Postage Stamp Home");
+?>
+
+<form method="post">
+<!-- Whole page is a single column table -->
+
+
+<table width=90% align=center>
+
+
+<!-- first row in the main table is the image selector UI which consists of a 2 x 3 table -->
+<tr>
+<td>
+
+<table width=100% align=center>
+
+<!-- first row of image selector is "Project" pulldown menu "Select Image By" radio boxes -->
+<tr>
+<td width=20% >
+  <table>
+    <tr><td><b>Project:</b>&nbsp;&nbsp;&nbsp;</td>
+        <td><select name="project">
+           <option <?php echo $gpc1_selected;?> >gpc1
+<!--
+           <option <?php echo $mops_selected;?> >megacam-mops
+           <option <?php echo $simtest_selected;?> >simtest
+-->
+        </td>
+    </tr>
+  </table>
+
+<td>
+&nbsp;<b>Select Images By:</b>&nbsp;&nbsp;&nbsp;
+<input type=radio name="select_by" value="db_id" <?php echo $file_checked; ?> >Database ID
+&nbsp;
+<input type=radio name="select_by" value="exposure_id" <?php echo $exp_checked; ?> >Exposure Name
+&nbsp;
+
+<input type=radio name="select_by" value="coord" <?php echo $coord_checked; ?> >Coordinates
+&nbsp;
+<input type=radio name="select_by" value="diff_image_id" <?php echo $diff_checked; ?> >Diff Image ID
+</td>
+</tr>
+
+
+<!-- the second row of the image selector table contains 
+    "Image Type" pulldown menu   "ID:" text input "Class ID" text input
+-->
+
+<tr>
+<td width=20%>
+<table>
+    <td><b>Image Type:</b>&nbsp;</td>
+    <td>
+        <select name="img_type">" >
+            <option <?php echo $chip_selected;?>  >chip
+            <option <?php echo $warp_selected ;?> >warp
+            <option <?php echo $stack_selected;?> >stack   
+            <option <?php echo $diff_selected;?>  >diff
+            <option <?php echo $raw_selected;?>   >raw
+        </select>
+    </td>
+</table>
+</td>
+
+<td>
+&nbsp;<b>ID/Name:</b>&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+<input type="text" name="id" value= <?php echo $rvar_id; ?> >
+&nbsp;&nbsp;&nbsp;&nbsp;
+<b>
+<?php if (0 && $rvar_project == "gpc1") {
+        echo "Chip ID:";
+      } else {
+        echo "Component:";
+      }
+?>
+</b>
+&nbsp;<input type="text" name="class_id" size=10 value="<?php echo $rvar_class_id;?>" >
+
+&nbsp;&nbsp;&nbsp;&nbsp;
+<!-- add text input field for Cell ID, not used yet -->
+
+<!--
+<b>Cell ID:</b>&nbsp;<input type="text" name="cell_id" size=10 value="<?php echo $rvar_cell_id;?>" >
+
+-->
+
+</td>
+
+</tr>
+
+</table> <!-- end of Image selector table -->
+
+<!-- a blank row for space-->
+<tr height=20><td></td></tr>
+
+<!-- next row of main table contains the ROI input UI -->
+<tr align=center width=100%>
+    <td>
+    <!-- 2 row by 6 column table -->
+    <table width=100%>
+    <thead>
+    <tr>
+    <!-- <td></td><td></td><td><b>Center</b></td><td></td><td></td><td><b>Range</b></td> -->
+    <td><td><td><b>Center</b><td><td><td><b>Range</b>
+    </tr>
+    </thead>
+    <tr>
+        <td>
+            <input type=radio name="center_type" value="Sky" <?php echo $sky_checked; ?> >Sky
+        </td>
+        <td>
+            &nbsp;
+            RA:
+            &nbsp;
+            &nbsp;
+            <input type="text" name="RA" size=10  value= <?php echo $rvar_RA; ?> >
+        </td>
+        <td>
+            &nbsp;
+            DEC:
+            &nbsp;
+            <input type="text" name="DEC" size=10 value="<?php echo $rvar_DEC;?>" >
+        </td>
+        <td>
+            <input type=radio name="range_type" value="Sky" <?php echo $rsky_checked; ?> >Sky
+        </td>
+        <td>
+            &nbsp;
+            dRA:
+            &nbsp;
+            &nbsp;
+            <input type="text" name="dRA" size=10  value= <?php echo $rvar_dRA; ?> >
+            &nbsp; "
+        </td>
+        <td>
+            &nbsp;
+            dDEC:
+            &nbsp;
+            <input type="text" name="dDEC" size=10 value="<?php echo $rvar_dDEC;?>" >
+            &nbsp; "
+        </td>
+    </tr>
+    <tr>
+        <td>
+            <input type=radio name="center_type" value="Pixels" <?php echo $pix_checked; ?> >Pixels
+        </td>
+        <td>
+            &nbsp;
+            X:
+            &nbsp;
+            &nbsp;
+            &nbsp;
+            <input type="text" name="X" size=10  value= <?php echo $rvar_X; ?> >
+        </td>
+        <td>
+            &nbsp;
+            Y:
+            &nbsp;
+            &nbsp;
+            &nbsp;
+            <input type="text" name="Y" size=10 value="<?php echo $rvar_Y;?>" >
+        </td>
+        <td>
+            <input type=radio name="range_type" value="Pixels" <?php echo $rpix_checked; ?> >Pixels
+        </td>
+        <td>
+            &nbsp;
+            width:
+            &nbsp;
+            &nbsp;
+            <input type="text" name="W" size=10  value= <?php echo $rvar_W; ?> >
+        </td>
+        <td>
+            &nbsp;
+            height:
+            &nbsp;
+            <input type="text" name="H" size=10 value="<?php echo $rvar_H;?>" >
+        </td>
+    </tr>
+    </table>
+    </td>
+</tr>
+
+
+<!-- next row of the main table contains the Submit button and the Mode radio buttons -->
+
+<tr align=center width=100% height=50>
+  <td>
+  <table width=80%>
+  <tr>
+
+    <td><input type=submit value="Submit"></td>
+    <td><b>Mode:</b>&nbsp;&nbsp;
+    <input type=radio name="cmd_mode" value="Get Status"<?php echo $getstatus_checked; ?> >Get Status
+    <input type=radio name="cmd_mode" value="Make Stamps"<?php echo $pstamp_checked; ?> >Make Stamps
+    <input type=radio name="cmd_mode" value="Get Images" <?php echo $get_checked; ?> >Get Bundles
+<!--
+    <input type=radio name="cmd_mode" value="List Images" <?php echo $list_checked; ?> >List Images
+-->
+    </td>
+<?php
+  // echo "<td><b>Request Id: $request_id";
+?>
+
+  </tr>
+  </table>
+  </td>
+</tr>
+
+<!--- next row in the main table is the Results of the previous request --->
+
+<tr>
+<!--- Don't show the command
+    <td>
+    <b>Command:</b>&nbsp;&nbsp; <?php echo "$command_line\n";?>
+    </td>
+--->
+
+</tr>
+<tr>
+    <td>
+    <b>Last Command</b>
+    </td>
+</tr>
+<tr>
+    <td>
+    <b>Status:</b>&nbsp;&nbsp;&nbsp; <?php echo "$command_status\n";?>
+    </td>
+</tr>
+<tr>
+    <td>
+    <b>Error:</b>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
+        <?php 
+            if ($error_line) {
+                echo "<pre>";
+                echo "$error_line\n";
+                echo "<pre>\n";
+            }
+        ?>
+    </td>
+</tr>
+
+<!-- a blank row for space -->
+<tr height=20><td> </td></tr>
+
+<tr>
+<td>
+<table align=center width=100% rules=none>
+<caption height=10 valign=center><b>Request Results</b></caption>
+
+<?php 
+if (0) {
+    // This is the old way of listing the status of the current request.
+    // now we save the submitted requests in the session see listRequests() below
+
+    $size = sizeof($output_array);
+    // echo "<pre>size of output array is $size\n</pre>";
+    if ($command_status == 0) {
+        if ($list_checked) {
+            // in list mode the output is a list of image files, just list them
+            // later we might add links to cause a stamp to be made from a selected file
+            for ($i = 0; $i < $size; $i++)  {
+                // $uri = array_shift($output_array);
+                $uri = $output_array[$i];
+                echo "<tr><td>$uri</td></tr>";
+            }
+        } else {
+            // output the list of urls
+            for ($i = 0; $i < $size; $i++)  {
+                // $uri = array_shift($output_array);
+                $uri = $output_array[$i];
+                printURL($uri);
+            }
+        }
+    }
+} // end if if(0)
+?>
+</table>
+</td>
+</tr>
+<!-- a blank row for our hidden element here-->
+
+<!-- request_id being non-zero causes us to issue status requests instead of new requsts -->
+<!--
+
+need a way to cancel a request There's probably no reason to have the value hidden, but we do
+need to set it as the last thing that we do
+
+-->
+
+<tr height=20>
+<td>
+    <input type="hidden" name="request_id" value=<?php echo $request_id ?> >
+</td>
+<td>
+    <input type="hidden" name="last_request_id" value=<?php echo $last_request_id ?> >
+</td>
+<td>
+    <input type="hidden" name="last_cmd_mode" value=<?php echo "\"$rvar_cmd_mode\"" ?> >
+</td>
+</tr>
+</table>
+
+<?php
+    listRequests("http://datastore.ipp.ifa.hawaii.edu/pstampresults", "pstamp_results_fileset");
+?>
+
+<!-- The end -->
+
+<p>
+<pre>
+
+
+<?php 
+
+//    echo "select_by: $rvar_select_by diff_checked: $diff_checked exp_checked: $exp_checked file_checked: $file_checked coord_checked: $coord_checked\n";
+
+    // dump parameters 
+
+    // phpinfo(32);
+
+?>
+
+</form>
+</body>
+</html>
Index: /branches/ccl_branches/ipponly-20191108/pstamp/web/upload.php
===================================================================
--- /branches/ccl_branches/ipponly-20191108/pstamp/web/upload.php	(revision 41103)
+++ /branches/ccl_branches/ipponly-20191108/pstamp/web/upload.php	(revision 41103)
@@ -0,0 +1,115 @@
+<?php // upload.php
+
+// get the locatl configuration variables
+include "pstamp.php";
+
+
+$user = $_SERVER['PHP_AUTH_USER'];
+$passwd = $_SERVER['PHP_AUTH_PW'];
+echo "<HTML>
+<head>
+    <title>
+        Upload Postage Stamp Request File
+    </title>
+<body>
+";
+
+// echo "Hello $user $passwd";
+
+echo <<<_END
+    <form method="post" enctype="multipart/form-data" action="">
+        <label>Postage Stamp Request File:
+        <input type="file" name='filename' accept='image/x-fits' /></label>
+    <br />
+    <br />
+    <input type="submit" value="Upload" />
+    &nbsp; &nbsp; &nbsp;
+    <input type="reset" name="cancel" value="Cancel"/>
+    <br />
+    </form>
+    <br />
+_END;
+
+$command = "";
+if ($_FILES) {
+    $name = $_FILES['filename']['name'];
+    $tmp_name = $_FILES['filename']['tmp_name'];
+    $type = $_FILES['filename']['type'];
+    $size = $_FILES['filename']['size'];
+
+    if ($name && ($size > 0)) {
+  //      echo "Uploaded $size bytes for '$type' file '$name' as '$tmp_name'<br />";
+        $command = "$SCRIPT pstamp_insert_request.pl --tmp_req_file $tmp_name --dbname $dbname --dbserver $dbserver --workdir $WORKDIR";
+    }
+}
+
+class Request
+{
+    public $id;
+    public $name;
+    function __construct($p1, $p2)
+    {
+        $this->id = $p1;
+        $this->name = $p2;
+    }
+}
+
+if (!isset($_SESSION['requests'])) {
+//    echo "initializing requests\n";
+//    echo "<br />";
+    $_SESSION['requests'] = array();
+}
+
+// echo "<br />SCRIPT is <br />$SCRIPT";
+$req_id = 0;
+$req_name = "";
+$new_request = 0;
+if ($command) {
+    $command = escapeshellcmd($command);
+   // echo "<br />command:<br />$command";
+    echo "<br />";
+    exec($command, $output, $command_status);
+    if ($command_status == 0) {
+        // it worked!
+        $req_id = $output[0];
+        // get rid of any whitespace in req_name
+        $req_name = trim($output[1]);
+        echo "Submitted Request ID:&nbsp; $req_id Request Name: &nbsp; $req_name<br \>";
+        $new_request = new Request($req_id, $req_name);
+        $num = count($_SESSION['requests']);
+//        echo "request array length: $num<br />";
+        $_SESSION['requests'][$num] = $new_request;
+        $num = count($_SESSION['requests']);
+//        echo "after request array length: $num<br />";
+    } else if ($command_status == 5) {
+        // PS_EXIT_DATA_ERROR
+        echo "Error:&nbsp;&nbsp;&nbsp;";
+        for ($i=0; $i < count($output); $i++) {
+            echo $output[$i];
+        }
+        echo "<br />\n";
+    } else {
+        echo "Unexpected Error.<br /> insert command returned $command_status<br \>";
+    }
+}
+echo "<br />";
+
+foreach ($_SESSION['requests'] as $req) {
+    echo "<br />";
+    echo $req->id;
+    echo "&nbsp;&nbsp";
+    $name = $req->name;
+    echo "&nbsp;&nbsp";
+    // XXX: get this data store product location a configuation
+    echo "<a href=\"http://datastore.ipp.ifa.hawaii.edu/pstampresults/$name\" TARGET=\"upload_results_fileset\">$name</a>\n";
+}
+echo "<br />";
+
+// print lots of information
+// phpinfo(-1);
+// print the most useful variables
+// phpinfo(32);
+
+echo "</body></html>";
+
+?>
